-
Notifications
You must be signed in to change notification settings - Fork 105
fix ai clarify opencode headless credentials #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { afterEach, describe, expect, it } from "bun:test"; | ||
| import { formatMissingApiKeyError, formatProviderCredentialHint, streamSimple } from "@gajae-code/ai/stream"; | ||
| import type { Context, Model } from "@gajae-code/ai/types"; | ||
|
|
||
| const originalOpenCodeApiKey = Bun.env.OPENCODE_API_KEY; | ||
|
|
||
| afterEach(() => { | ||
| if (originalOpenCodeApiKey === undefined) delete Bun.env.OPENCODE_API_KEY; | ||
| else Bun.env.OPENCODE_API_KEY = originalOpenCodeApiKey; | ||
| }); | ||
|
|
||
| describe("formatProviderCredentialHint", () => { | ||
| it("explains OpenCode Go subscription auth and the headless signal (#755)", () => { | ||
| const hint = formatProviderCredentialHint("opencode-go"); | ||
| expect(hint).toContain("OpenCode subscriptions authenticate with an API key"); | ||
| expect(hint).toContain("https://opencode.ai/auth"); | ||
| expect(hint).toContain("not a separate session/OAuth token"); | ||
| expect(hint).toContain("OPENCODE_API_KEY"); | ||
| expect(hint).toContain("~/.gjc/.env"); | ||
| expect(hint).toContain("project .env is intentionally ignored"); | ||
| expect(hint).toContain("once before headless/print mode to store the key interactively"); | ||
| expect(hint).not.toContain("non-interactively"); | ||
| }); | ||
|
|
||
| it("covers opencode-zen with the same shape", () => { | ||
| const hint = formatProviderCredentialHint("opencode-zen"); | ||
| expect(hint).toContain("OPENCODE_API_KEY"); | ||
| expect(hint).toContain("gjc auth-broker login opencode-zen"); | ||
| }); | ||
|
|
||
| it("names the env var for a plain env-key provider without an OpenCode note or invalid login command", () => { | ||
| const hint = formatProviderCredentialHint("groq"); | ||
| expect(hint).toContain("GROQ_API_KEY"); | ||
| expect(hint).toContain("project .env is intentionally ignored"); | ||
| expect(hint).not.toContain("OpenCode"); | ||
| // groq is not an auth-broker OAuth provider, so we must not suggest a login that would fail. | ||
| expect(hint).not.toContain("gjc auth-broker login"); | ||
| }); | ||
|
|
||
| it("returns an empty hint for providers without a static env-var key", () => { | ||
| // anthropic resolves via a function (OAuth/foundry), so there is no single env var to name. | ||
| expect(formatProviderCredentialHint("anthropic")).toBe(""); | ||
| expect(formatProviderCredentialHint("totally-unknown-provider")).toBe(""); | ||
| }); | ||
| }); | ||
|
|
||
| describe("formatMissingApiKeyError", () => { | ||
| it("prefixes the base error and appends OpenCode guidance", () => { | ||
| const message = formatMissingApiKeyError("opencode-go"); | ||
| expect(message).toContain("No API key for provider: opencode-go."); | ||
| expect(message).toContain("OPENCODE_API_KEY"); | ||
| expect(message).toContain("once before headless/print mode to store the key interactively"); | ||
| }); | ||
|
|
||
| it("falls back to the bare base error for providers with no hint", () => { | ||
| expect(formatMissingApiKeyError("anthropic")).toBe("No API key for provider: anthropic."); | ||
| }); | ||
| }); | ||
|
|
||
| describe("streamSimple missing credentials", () => { | ||
| it("throws OpenCode Go headless guidance on the actual no-key path (#755)", () => { | ||
| delete Bun.env.OPENCODE_API_KEY; | ||
| const model: Model<"openai-completions"> = { | ||
| api: "openai-completions", | ||
| provider: "opencode-go", | ||
| id: "deepseek-v4-flash", | ||
| name: "DeepSeek V4 Flash", | ||
| baseUrl: "https://opencode.ai/zen/go/v1", | ||
| }; | ||
| const context: Context = { | ||
| messages: [{ role: "user", content: "hi", timestamp: 0 }], | ||
| }; | ||
|
|
||
| expect(() => streamSimple(model, context)).toThrow("OpenCode subscriptions authenticate with an API key"); | ||
| expect(() => streamSimple(model, context)).toThrow("OPENCODE_API_KEY"); | ||
| expect(() => streamSimple(model, context)).toThrow("not a separate session/OAuth token"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { formatProviderCredentialHint } from "@gajae-code/ai/stream"; | ||
|
|
||
| export const MODEL_ONBOARDING_API_PROVIDER_COMMAND = | ||
| "/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>"; | ||
| export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>"; | ||
|
|
@@ -26,14 +28,19 @@ export function formatNoModelOnboardingError(): string { | |
| } | ||
|
|
||
| export function formatNoCredentialOnboardingError(providerId: string): string { | ||
| return [ | ||
| const lines = [ | ||
| `No credentials found for ${providerId}.`, | ||
| "", | ||
| `For MiniMax/GLM presets, configure credentials with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`, | ||
| `For custom API-compatible providers, use ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`, | ||
| `For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`, | ||
| `For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND} (interactive; not available in headless/print mode).`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This changes Useful? React with 👍 / 👎. |
||
| ]; | ||
| const headlessHint = formatProviderCredentialHint(providerId); | ||
| if (headlessHint) lines.push(headlessHint); | ||
| lines.push( | ||
| "Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.", | ||
| ].join("\n"); | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
|
|
||
| export function formatNoModelsAvailableFallback(): string { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
OPENCODE_API_KEYis exported or present in a user/GJC env file,$credentialEnvhas already captured that value while importing@gajae-code/ai/stream, sodelete Bun.env.OPENCODE_API_KEYhere does not makegetEnvApiKey("opencode-go")return undefined. In that environmentstreamSimpledoes not throw the missing-key guidance (and may start a real provider request), making this new regression test fail for contributors/CI with OpenCode credentials; run the assertion in an isolated process/env before importingstreamor otherwise suppress the credential snapshots.Useful? React with 👍 / 👎.