opencode-cmd-provider 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +2 -0
- package/dist/src/deals/catalog.js +2 -0
- package/dist/src/deals/plan-summary.d.ts +33 -1
- package/dist/src/deals/plan-summary.js +35 -19
- package/dist/src/env.d.ts +9 -0
- package/dist/src/env.js +11 -0
- package/dist/src/plugin/auth-mirror.d.ts +11 -0
- package/dist/src/plugin/auth-mirror.js +74 -0
- package/dist/src/plugin/auth.d.ts +3 -0
- package/dist/src/plugin/auth.js +12 -0
- package/dist/src/provider/command-code-model.d.ts +49 -0
- package/dist/src/provider/command-code-model.js +419 -171
- package/dist/src/provider/converters.d.ts +18 -0
- package/dist/src/provider/converters.js +464 -3
- package/dist/src/provider/redact.d.ts +11 -0
- package/dist/src/provider/redact.js +38 -0
- package/dist/src/provider/stream.d.ts +6 -0
- package/dist/src/provider/stream.js +467 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 - 2026-08-23
|
|
4
|
+
|
|
5
|
+
Feature: after a successful `/connect`, the credential is mirrored under
|
|
6
|
+
`command-code` in OpenCode's auth store and to `~/.commandcode/auth.json`
|
|
7
|
+
(official CLI layout), so ecosystem consumers such as OpenChamber's Usage
|
|
8
|
+
page find the key without manual setup.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- **Credential mirroring** (issue #64): `/connect` now writes the credential
|
|
13
|
+
under `command-code` in OpenCode's auth store — refreshed on every
|
|
14
|
+
successful re-auth so the mirror stays in sync — and to
|
|
15
|
+
`~/.commandcode/auth.json` in the official CLI layout, which is only
|
|
16
|
+
written when it does not already hold a different credential, so an
|
|
17
|
+
official CLI login is never clobbered. Writes are atomic; new auth files
|
|
18
|
+
get owner-only permissions and existing file modes are preserved.
|
|
19
|
+
- Mirroring is best-effort: failures are swallowed and never fail `/connect`
|
|
20
|
+
itself, and `mirror: false` opts out (used by the oauth tests). Users who
|
|
21
|
+
authenticate via `COMMANDCODE_API_KEY` alone are unaffected.
|
|
22
|
+
- Existing users re-run `/connect` once (or copy the entry manually) to pick
|
|
23
|
+
the mirror up.
|
|
24
|
+
|
|
25
|
+
### Chores
|
|
26
|
+
|
|
27
|
+
- New `tests/auth-mirror.test.ts` suite: preserves unrelated auth-store
|
|
28
|
+
entries, owner-only permissions on create, stale-entry refresh on re-auth,
|
|
29
|
+
no-clobber of a differing CLI credential, idempotent no-op, blank-key
|
|
30
|
+
guard, and the end-to-end `/connect` flow including `mirror: false` —
|
|
31
|
+
wired into `test:unit`.
|
|
32
|
+
- Docs: README notes the mirror locations and that mirroring is best-effort.
|
|
33
|
+
|
|
34
|
+
## 1.3.0 - 2026-08-23
|
|
35
|
+
|
|
36
|
+
Feature: dual-transport Provider API — non-Go plans now use the documented
|
|
37
|
+
`/provider/v1/*` endpoints with self-healing fallback to the legacy transport.
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
- **Provider API routing** (issues #51, #53): non-Go plans (goat, pro, max,
|
|
42
|
+
max20, teampro, provider aliases) route through the documented Provider API —
|
|
43
|
+
`claude-*` models to `POST /provider/v1/messages` (Anthropic shape), everything
|
|
44
|
+
else to `POST /provider/v1/chat/completions` (OpenAI shape) — with retry,
|
|
45
|
+
timeout, abort, and redaction parity with the legacy transport. Go /
|
|
46
|
+
individual-go sessions stay byte-for-byte on the legacy `POST /alpha/generate`
|
|
47
|
+
wire format, proven by golden byte-parity tests.
|
|
48
|
+
- **Session-cached plan resolution** (issue #54): transport is chosen per model
|
|
49
|
+
instance from the shared plan-resolution seam — explicit override →
|
|
50
|
+
`COMMANDCODE_PLAN` env → cached `GET /alpha/whoami` → default Provider API.
|
|
51
|
+
Only a resolved `go` selects the legacy transport; next session after a plan
|
|
52
|
+
upgrade auto-switches.
|
|
53
|
+
- **Self-healing upgrade flip** (issue #56): if a plan-detection miss sends a
|
|
54
|
+
true Go user to the Provider API, a documented `403 upgrade_required` pins the
|
|
55
|
+
session to the legacy transport and retries the same call once there — no
|
|
56
|
+
second Provider API hit, no double-counted usage.
|
|
57
|
+
- **ZDR passthrough** (issue #57): `CMD_ZDR=1` sends `x-cmd-zdr: 1` on every
|
|
58
|
+
Provider API request; the documented `422 cmd_zdr_no_providers` flows through
|
|
59
|
+
the existing error/redaction pipeline. The legacy transport never sends it.
|
|
60
|
+
- **Transport hardening** (issue #58): the finish part now waits for a trailing
|
|
61
|
+
OpenAI usage-only chunk so cost reflects real token counts; non-image file
|
|
62
|
+
parts are rejected with a clear role-aware error instead of silently
|
|
63
|
+
base64-encoding; stateful SSE parsers complete tool calls whose arguments
|
|
64
|
+
arrive across multiple events.
|
|
65
|
+
|
|
66
|
+
### Fixes
|
|
67
|
+
|
|
68
|
+
- Restored the "Command Code" TUI sidebar section for Ox Alpha and DeepSeek V4
|
|
69
|
+
Flash Vision (exp): the deals-coverage gate now fails loudly when scraped
|
|
70
|
+
records lack a snapshot model, and the fixtures were refreshed to cover every
|
|
71
|
+
model (issue #61).
|
|
72
|
+
|
|
73
|
+
### Chores
|
|
74
|
+
|
|
75
|
+
- Added the `refresh` project skill documenting the offline catalog refresh
|
|
76
|
+
(`npm run refresh` from `tests/fixtures/*.html`).
|
|
77
|
+
- New test suites: provider transport, parity, upgrade-fallback, ZDR, and
|
|
78
|
+
deals coverage — all wired into `test:unit`.
|
|
79
|
+
|
|
3
80
|
## 1.2.2 - 2026-08-22
|
|
4
81
|
|
|
5
82
|
Chore: catalog refresh to `command-code@1.32.1`.
|
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ Select **Command Code**, complete the browser flow, and pick a model with `/mode
|
|
|
49
49
|
|
|
50
50
|
Run `/connect` in OpenCode and select **Command Code**. The browser flow stores the returned credential in OpenCode's auth store.
|
|
51
51
|
|
|
52
|
+
The credential is also mirrored under `command-code` in OpenCode's auth store and to `~/.commandcode/auth.json` (official CLI layout, only written when that file does not already hold a different credential). Ecosystem consumers such as OpenChamber's Usage page read those locations. Mirroring is best-effort; if it fails, `/connect` still succeeds.
|
|
53
|
+
|
|
52
54
|
If automatic transfer from the browser fails, copy the API key shown by Command Code and export it as `COMMANDCODE_API_KEY` (see below).
|
|
53
55
|
|
|
54
56
|
### Environment variable
|
|
@@ -13,6 +13,7 @@ export const MODEL_DEALS = {
|
|
|
13
13
|
"claude-sonnet-4-6": { tier: "premium", benchmark: { "intelligence": 48.4 }, free: false, allowance: { "pro": 20 } },
|
|
14
14
|
"claude-sonnet-5": { tier: "premium", benchmark: { "intelligence": 55.3, "tokPerSec": 81.9 }, free: false, allowance: { "pro": 20 } },
|
|
15
15
|
"deepseek/deepseek-v4-flash": { tier: "opensource", benchmark: { "intelligence": 51.8, "tokPerSec": 114.6 }, peakOffPeak: { "peak": { "input": 0.44, "output": 1.32, "cacheRead": 0.014, "cacheWrite": 0 }, "offPeak": { "input": 0.22, "output": 0.66, "cacheRead": 0.007, "cacheWrite": 0 }, "windows": "01–04 & 06–10 UTC" }, free: false, allowance: { "goat": 60, "pro": 70 } },
|
|
16
|
+
"deepseek/deepseek-v4-flash-vision-exp": { tier: "opensource", peakOffPeak: { "peak": { "input": 0.44, "output": 1.32, "cacheRead": 0.02, "cacheWrite": 0 }, "offPeak": { "input": 0.22, "output": 0.66, "cacheRead": 0.01, "cacheWrite": 0 }, "windows": "01–04 & 06–10 UTC" }, free: false, allowance: { "goat": 20, "pro": 30 } },
|
|
16
17
|
"deepseek/deepseek-v4-pro": { tier: "opensource", benchmark: { "intelligence": 53.2, "tokPerSec": 75.3 }, peakOffPeak: { "peak": { "input": 1.32, "output": 3.96, "cacheRead": 0.044, "cacheWrite": 0 }, "offPeak": { "input": 0.66, "output": 1.98, "cacheRead": 0.022, "cacheWrite": 0 }, "windows": "01–04 & 06–10 UTC" }, free: false, allowance: { "goat": 20, "pro": 30 } },
|
|
17
18
|
"google/gemini-3.1-flash-lite": { tier: "premium", benchmark: { "intelligence": 25.6 }, free: false, allowance: { "pro": 20 } },
|
|
18
19
|
"google/gemini-3.5-flash": { tier: "premium", benchmark: { "intelligence": 52 }, free: false, allowance: { "pro": 20 } },
|
|
@@ -47,6 +48,7 @@ export const MODEL_DEALS = {
|
|
|
47
48
|
"Qwen/Qwen3.8-27B": { tier: "opensource", benchmark: { "intelligence": 52 }, free: false, allowance: { "goat": 70, "pro": 80 } },
|
|
48
49
|
"Qwen/Qwen3.8-Max": { tier: "opensource", benchmark: { "intelligence": 58.1, "tokPerSec": 46.7 }, free: false, allowance: { "goat": 20, "pro": 30 } },
|
|
49
50
|
"sakana/fugu-ultra": { tier: "premium", free: false },
|
|
51
|
+
"stealth/ox-alpha": { tier: "opensource", free: true },
|
|
50
52
|
"stepfun/Step-3.5-Flash": { tier: "opensource", benchmark: { "intelligence": 26.5 }, free: false, allowance: { "goat": 20, "pro": 30 } },
|
|
51
53
|
"stepfun/Step-3.7-Flash": { tier: "opensource", benchmark: { "intelligence": 30.9, "tokPerSec": 120.1 }, free: false, allowance: { "goat": 20, "pro": 30 } },
|
|
52
54
|
"tencent/hy3-paid": { tier: "opensource", benchmark: { "intelligence": 42.2, "tokPerSec": 74.8 }, free: false, allowance: { "goat": 70, "pro": 80 } },
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type ModelDeals, type PlanId, type PlanInfo } from "./catalog.js";
|
|
3
3
|
export declare function normalizePlan(value: unknown): PlanId | undefined;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Per-instance whoami cache. Passed by transport selection so the
|
|
6
|
+
* `GET /alpha/whoami` fetch happens at most once per model instance — even
|
|
7
|
+
* under concurrent first calls, because the cache holds the in-flight
|
|
8
|
+
* attempt; the Deals tool path resolves without a cache (fetch per call, as
|
|
9
|
+
* before).
|
|
10
|
+
*/
|
|
11
|
+
export interface PlanResolutionCache {
|
|
12
|
+
/** The whoami attempt (in flight or settled); its `plan` is the resolved
|
|
13
|
+
* plan or undefined when the fetch failed / no key was present. */
|
|
14
|
+
whoami?: Promise<{
|
|
15
|
+
plan: PlanId | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvePlanOptions {
|
|
19
|
+
/** Fallback when no override resolves and whoami yields nothing. Deals
|
|
20
|
+
* intelligence keeps the "go" default; transport selection passes
|
|
21
|
+
* "provider". */
|
|
22
|
+
defaultPlan?: PlanId;
|
|
23
|
+
/** Per-instance whoami cache (see PlanResolutionCache). */
|
|
24
|
+
cache?: PlanResolutionCache;
|
|
25
|
+
/** Resolved API key for the whoami GET (defaults to the
|
|
26
|
+
* COMMANDCODE_API_KEY env var); transport selection passes the model's
|
|
27
|
+
* resolved key. */
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
/** Base URL for the whoami GET (defaults to getApiBase(env)); transport
|
|
30
|
+
* selection passes the model's baseURL option. */
|
|
31
|
+
baseURL?: string;
|
|
32
|
+
/** Fetch implementation for the whoami GET (defaults to the global fetch),
|
|
33
|
+
* so transport selection honours the same injected fetch as inference. */
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
export declare function resolvePlan(planArg: string | undefined, env?: NodeJS.ProcessEnv, options?: ResolvePlanOptions): Promise<PlanId>;
|
|
5
37
|
export declare function renderPlanSummary(plan: PlanId, deals?: Readonly<Record<string, ModelDeals>>, catalog?: Readonly<Record<PlanId, PlanInfo>>): string;
|
|
6
38
|
export declare function planSummaryTool(): {
|
|
7
39
|
description: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/deals/plan-summary.ts — cmd_plan_summary tool: plan-aware allowance
|
|
2
2
|
// breakdown. Plan resolution: tool arg → COMMANDCODE_PLAN → live /alpha/whoami
|
|
3
|
-
// (when a key is present and the network works) → default
|
|
4
|
-
//
|
|
3
|
+
// (when a key is present and the network works) → default. Deals intelligence
|
|
4
|
+
// keeps the "go" default; transport selection reuses resolvePlan() with
|
|
5
|
+
// { defaultPlan: "provider" } so only a resolved "go" picks the legacy
|
|
6
|
+
// transport. Rendering is a pure function so tests never touch the network.
|
|
5
7
|
import { z } from "zod";
|
|
6
8
|
import { MODEL_COSTS } from "../catalog/facts.js";
|
|
7
9
|
import { MODEL_DEALS, PLAN_CATALOG, } from "./catalog.js";
|
|
@@ -44,31 +46,45 @@ export function normalizePlan(value) {
|
|
|
44
46
|
return undefined;
|
|
45
47
|
return PLAN_ALIASES[value.toLowerCase()];
|
|
46
48
|
}
|
|
47
|
-
export async function resolvePlan(planArg, env = process.env) {
|
|
49
|
+
export async function resolvePlan(planArg, env = process.env, options = {}) {
|
|
48
50
|
const fromArg = normalizePlan(planArg);
|
|
49
51
|
if (fromArg)
|
|
50
52
|
return fromArg;
|
|
51
53
|
const fromEnv = normalizePlan(env.COMMANDCODE_PLAN);
|
|
52
54
|
if (fromEnv)
|
|
53
55
|
return fromEnv;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
const cache = options.cache;
|
|
57
|
+
const whoamiPlan = cache?.whoami
|
|
58
|
+
? (await cache.whoami).plan
|
|
59
|
+
: (await fetchWhoamiPlan(env, cache, options)).plan;
|
|
60
|
+
return whoamiPlan ?? options.defaultPlan ?? "go";
|
|
61
|
+
}
|
|
62
|
+
async function fetchWhoamiPlan(env, cache, options) {
|
|
63
|
+
const key = options.apiKey ?? env.COMMANDCODE_API_KEY;
|
|
64
|
+
const base = options.baseURL ?? getApiBase(env);
|
|
65
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
66
|
+
const attempt = (async () => {
|
|
67
|
+
let plan;
|
|
68
|
+
if (key) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetchImpl(`${base}/alpha/whoami`, {
|
|
71
|
+
headers: { authorization: `Bearer ${key}` },
|
|
72
|
+
signal: AbortSignal.timeout(5000),
|
|
73
|
+
});
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
plan = normalizePlan(body.planId ?? body.plan?.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// offline, timeout or unreachable — fall through to the default
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
83
|
+
return { plan };
|
|
84
|
+
})();
|
|
85
|
+
if (cache)
|
|
86
|
+
cache.whoami = attempt;
|
|
87
|
+
return attempt;
|
|
72
88
|
}
|
|
73
89
|
const REQUEST_PROFILE = { input: 800, output: 200, cacheRead: 50_000 };
|
|
74
90
|
export function renderPlanSummary(plan, deals = MODEL_DEALS, catalog = PLAN_CATALOG) {
|
package/dist/src/env.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
export declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2
2
|
export declare function getApiBase(env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
/**
|
|
4
|
+
* Zero data retention (ZDR) opt-in, mirroring the CLI's `CMD_ZDR=1` (per the
|
|
5
|
+
* Provider API docs: "the same opt-in the CLI exposes via CMD_ZDR=1"). Only
|
|
6
|
+
* the exact value `1` opts in — unset, empty, `0`, or any other value leaves
|
|
7
|
+
* ZDR off, so no `x-cmd-zdr` header is ever sent unless the user explicitly
|
|
8
|
+
* asked for it. The legacy /alpha/generate transport never sends the header
|
|
9
|
+
* regardless of this value.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCmdZdr(env?: NodeJS.ProcessEnv): boolean;
|
package/dist/src/env.js
CHANGED
|
@@ -3,3 +3,14 @@ export const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
|
3
3
|
export function getApiBase(env = process.env) {
|
|
4
4
|
return env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Zero data retention (ZDR) opt-in, mirroring the CLI's `CMD_ZDR=1` (per the
|
|
8
|
+
* Provider API docs: "the same opt-in the CLI exposes via CMD_ZDR=1"). Only
|
|
9
|
+
* the exact value `1` opts in — unset, empty, `0`, or any other value leaves
|
|
10
|
+
* ZDR off, so no `x-cmd-zdr` header is ever sent unless the user explicitly
|
|
11
|
+
* asked for it. The legacy /alpha/generate transport never sends the header
|
|
12
|
+
* regardless of this value.
|
|
13
|
+
*/
|
|
14
|
+
export function getCmdZdr(env = process.env) {
|
|
15
|
+
return env.CMD_ZDR === "1";
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MirrorOptions {
|
|
2
|
+
opencodeAuthFile?: string;
|
|
3
|
+
legacyAuthFile?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MirrorResult {
|
|
6
|
+
opencodeAuthUpdated: boolean;
|
|
7
|
+
legacyAuthUpdated: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function defaultOpencodeAuthFile(): string;
|
|
10
|
+
export declare function defaultLegacyAuthFile(): string;
|
|
11
|
+
export declare function mirrorCredential(key: string, options?: MirrorOptions): MirrorResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/plugin/auth-mirror.ts — mirror /connect credentials for ecosystem consumers
|
|
2
|
+
//
|
|
3
|
+
// OpenChamber's command-code quota provider reads the API key from OpenCode's
|
|
4
|
+
// auth store under the key "command-code" (the provider id its own archived
|
|
5
|
+
// plugin used). OpenCode stores this plugin's credential under "commandcode",
|
|
6
|
+
// so after a successful /connect we mirror it under "command-code" as well.
|
|
7
|
+
// We also mirror to ~/.commandcode/auth.json in the official CLI layout,
|
|
8
|
+
// which resolveApiKey already reads as a legacy fallback.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
function dataHome() {
|
|
13
|
+
return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
14
|
+
}
|
|
15
|
+
export function defaultOpencodeAuthFile() {
|
|
16
|
+
return join(dataHome(), "opencode", "auth.json");
|
|
17
|
+
}
|
|
18
|
+
export function defaultLegacyAuthFile() {
|
|
19
|
+
return join(homedir(), ".commandcode", "auth.json");
|
|
20
|
+
}
|
|
21
|
+
function readJsonObject(path) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
24
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Missing or malformed file: start from an empty document.
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
function writeJsonAtomic(path, value) {
|
|
34
|
+
const dir = join(path, "..");
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
const existingMode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
|
|
37
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
|
|
38
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: existingMode });
|
|
39
|
+
renameSync(tmp, path);
|
|
40
|
+
}
|
|
41
|
+
function isCredentialRecord(value) {
|
|
42
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
// OpenCode's auth store is owned by this plugin's ecosystem entry point, so a
|
|
45
|
+
// stale mirror there is refreshed on every successful /connect. The legacy
|
|
46
|
+
// file belongs to the official Command Code CLI; if it already holds a
|
|
47
|
+
// different credential we leave it alone rather than clobber another tool's
|
|
48
|
+
// login.
|
|
49
|
+
export function mirrorCredential(key, options = {}) {
|
|
50
|
+
const trimmed = key.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return { opencodeAuthUpdated: false, legacyAuthUpdated: false };
|
|
53
|
+
const entry = { type: "api", key: trimmed };
|
|
54
|
+
const opencodeAuthFile = options.opencodeAuthFile ?? defaultOpencodeAuthFile();
|
|
55
|
+
const opencodeDoc = readJsonObject(opencodeAuthFile);
|
|
56
|
+
const currentOpencode = opencodeDoc["command-code"];
|
|
57
|
+
let opencodeAuthUpdated = false;
|
|
58
|
+
if (!isCredentialRecord(currentOpencode) ||
|
|
59
|
+
currentOpencode.type !== "api" ||
|
|
60
|
+
currentOpencode.key !== trimmed) {
|
|
61
|
+
opencodeDoc["command-code"] = entry;
|
|
62
|
+
writeJsonAtomic(opencodeAuthFile, opencodeDoc);
|
|
63
|
+
opencodeAuthUpdated = true;
|
|
64
|
+
}
|
|
65
|
+
const legacyAuthFile = options.legacyAuthFile ?? defaultLegacyAuthFile();
|
|
66
|
+
const legacyDoc = readJsonObject(legacyAuthFile);
|
|
67
|
+
let legacyAuthUpdated = false;
|
|
68
|
+
if (!isCredentialRecord(legacyDoc["command-code"])) {
|
|
69
|
+
legacyDoc["command-code"] = entry;
|
|
70
|
+
writeJsonAtomic(legacyAuthFile, legacyDoc);
|
|
71
|
+
legacyAuthUpdated = true;
|
|
72
|
+
}
|
|
73
|
+
return { opencodeAuthUpdated, legacyAuthUpdated };
|
|
74
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type MirrorOptions } from "./auth-mirror.js";
|
|
1
2
|
import type { AuthOAuthResult } from "@opencode-ai/plugin";
|
|
2
3
|
export interface RunAuthFlowOptions {
|
|
3
4
|
startPort?: number;
|
|
4
5
|
timeoutMs?: number;
|
|
6
|
+
/** Credential mirroring targets; pass `false` to disable mirroring. */
|
|
7
|
+
mirror?: MirrorOptions | false;
|
|
5
8
|
}
|
|
6
9
|
export declare function runAuthFlow(options?: RunAuthFlowOptions): Promise<AuthOAuthResult>;
|
package/dist/src/plugin/auth.js
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
// Wraps the local callback server in the opencode AuthOAuthResult shape:
|
|
4
4
|
// the studio URL is opened in the browser, the studio POSTs the API key to
|
|
5
5
|
// the local /callback endpoint, and callback() resolves with the key.
|
|
6
|
+
// On success the credential is also mirrored under "command-code" (see
|
|
7
|
+
// auth-mirror.ts) so ecosystem consumers such as OpenChamber's quota
|
|
8
|
+
// provider can find it.
|
|
6
9
|
import { randomBytes } from "node:crypto";
|
|
7
10
|
import { startAuthServer } from "./auth-server.js";
|
|
11
|
+
import { mirrorCredential } from "./auth-mirror.js";
|
|
8
12
|
const STUDIO_BASE_URL = "https://commandcode.ai";
|
|
9
13
|
const DEFAULT_AUTH_TIMEOUT_MS = 15_000;
|
|
10
14
|
function generateStateToken() {
|
|
@@ -27,6 +31,14 @@ export async function runAuthFlow(options = {}) {
|
|
|
27
31
|
authServer.server.close();
|
|
28
32
|
if (callback.state !== stateToken)
|
|
29
33
|
return { type: "failed" };
|
|
34
|
+
if (options.mirror !== false) {
|
|
35
|
+
try {
|
|
36
|
+
mirrorCredential(callback.apiKey, options.mirror ?? {});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Mirroring is best-effort; it must never fail the login itself.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
return { type: "success", key: callback.apiKey };
|
|
31
43
|
}
|
|
32
44
|
catch {
|
|
@@ -10,6 +10,7 @@ export interface CommandCodeModelOptions {
|
|
|
10
10
|
maxRetries?: number;
|
|
11
11
|
maxRetryDelayMs?: number;
|
|
12
12
|
authPaths?: readonly string[];
|
|
13
|
+
plan?: string;
|
|
13
14
|
}
|
|
14
15
|
export declare class CommandCodeLanguageModel implements LanguageModelV3 {
|
|
15
16
|
private readonly options;
|
|
@@ -22,6 +23,34 @@ export declare class CommandCodeLanguageModel implements LanguageModelV3 {
|
|
|
22
23
|
constructor(options: CommandCodeModelOptions, modelId: string);
|
|
23
24
|
private apiBase;
|
|
24
25
|
private costForModel;
|
|
26
|
+
/**
|
|
27
|
+
* Per-instance whoami cache: the `GET /alpha/whoami` fetch happens at most
|
|
28
|
+
* once for the lifetime of this model instance and is reused across turns.
|
|
29
|
+
*/
|
|
30
|
+
private readonly planCache;
|
|
31
|
+
/**
|
|
32
|
+
* Safety-net flag (issue #56): once the Provider API answers a documented
|
|
33
|
+
* `403 upgrade_required`, the session is pinned to the legacy
|
|
34
|
+
* `/alpha/generate` transport for the lifetime of this model instance —
|
|
35
|
+
* subsequent turns stay on legacy without re-hitting the Provider API (no
|
|
36
|
+
* second 403). The Provider API has no path for Go-plan users (that is
|
|
37
|
+
* exactly what the 403 documents), so the plugin's legacy transport is the
|
|
38
|
+
* only way to keep serving a plan-detection miss that routed a true Go user
|
|
39
|
+
* there.
|
|
40
|
+
*/
|
|
41
|
+
private pinnedToLegacy;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the transport plan through the shared plan-resolution seam:
|
|
44
|
+
* explicit override (providerOptions plan, model option `plan`) →
|
|
45
|
+
* COMMANDCODE_PLAN env → cached whoami → default Provider API. Only a
|
|
46
|
+
* resolved `go` selects the legacy transport; every other resolution
|
|
47
|
+
* selects the Provider API. The whoami fetch is cached for the lifetime of
|
|
48
|
+
* this instance (see planCache) and honours the same resolved key, base URL
|
|
49
|
+
* and injected fetch as inference.
|
|
50
|
+
*/
|
|
51
|
+
private shouldUseProviderTransport;
|
|
52
|
+
private planArgFor;
|
|
53
|
+
private providerEndpoint;
|
|
25
54
|
doGenerate(options: ModelCallOptions): Promise<LanguageModelV3GenerateResult>;
|
|
26
55
|
doStream(options: ModelCallOptions): Promise<{
|
|
27
56
|
stream: ReadableStream<LanguageModelV3StreamPart>;
|
|
@@ -35,5 +64,25 @@ export declare class CommandCodeLanguageModel implements LanguageModelV3 {
|
|
|
35
64
|
private runOnce;
|
|
36
65
|
private bodyFor;
|
|
37
66
|
private headersFor;
|
|
67
|
+
private providerBodyFor;
|
|
68
|
+
private providerHeadersFor;
|
|
69
|
+
private providerRunStream;
|
|
38
70
|
private runStream;
|
|
71
|
+
/**
|
|
72
|
+
* Deep internal seam: single SSE transport behind a small interface.
|
|
73
|
+
* All retry/timeout/abort/redaction/stream-parsing/cost/fallback logic
|
|
74
|
+
* lives here; callers supply only the endpoint URL, body, headers and
|
|
75
|
+
* the event→parts mapper. Depth gives leverage (N callers) and locality
|
|
76
|
+
* (fix once, fixed everywhere). The eventToParts adapter varies across
|
|
77
|
+
* the seam (CC vs OpenAI vs Anthropic) while the transport stays fixed.
|
|
78
|
+
*
|
|
79
|
+
* The optional legacyFallback implements the issue #56 safety net: when the
|
|
80
|
+
* Provider API answers a documented `403 upgrade_required` (Go plan, no API
|
|
81
|
+
* access), the session is pinned to the legacy `/alpha/generate` transport
|
|
82
|
+
* and the same call retries once there — the retry is bounded because only
|
|
83
|
+
* the provider descriptor carries flipOnUpgradeRequired. The pin is sticky
|
|
84
|
+
* for the lifetime of this model instance (no second Provider API hit on
|
|
85
|
+
* later turns).
|
|
86
|
+
*/
|
|
87
|
+
private transportStream;
|
|
39
88
|
}
|