Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@ OpenCodex provides official adapter support for Qoder through the `qoder` (Globa
([Global](https://qoder.com/account/integrations), [CN](https://qoder.cn/account/integrations)) and paste it as the provider's API key. The stored key reaches the CLI only as `QODER_PERSONAL_ACCESS_TOKEN` (Global) or `QODERCN_PERSONAL_ACCESS_TOKEN` (CN) in a scoped child environment.
- **Region Isolation:** Each preset accepts only its canonical destination (`https://qoder.com` or `https://qoder.cn`) and resolves its own executable. Credentials, model cache, usage, and health are independent; neither region falls back to the other. An older custom provider named `qoder` with a different destination keeps its existing adapter and URL.
- **Model Discovery:** `qoder --list-models` is the authoritative entitlement roster for the current PAT. The cache is bound to an irreversible fingerprint of the token, so switching accounts never reuses another account's roster. If discovery fails, the provider degrades to a stale cache and then the documented static seed.
- **Token usage:** Qoder CLI reports zero token counts unless its usage switch is on, so each preset turns it on in its scoped CLI child environment: `QODER_EXPOSE_TOKEN_USAGE=true` for Global and `QODERCN_EXPOSE_TOKEN_USAGE=true` for CN, which reads only its own prefix.
- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. Client-declared tools are exposed through an isolated request-scoped MCP server.
- **Image Input:** Qoder Global accepts image input through the Qoder CLI. OpenCodex forwards supported image inputs that reach the Global adapter to the CLI without maintaining a per-model vision capability allowlist there. Image compatibility and any model-specific preprocessing or routing are handled by Qoder. OpenCodex does not use the static discovery fallback roster to classify per-model vision capability. Qoder CN keeps its existing sidecar classification, and any residual raw-image request that reaches the CN adapter is rejected as `unsupported_input_modality`. OpenCodex serializes history before the current turn, but Qoder does not persist sessions, so prior images are replayed on later turns. In observed multi-turn image conversations, replayed images can become unreliably associated or effectively ordered relative to the historical text they originally accompanied.
- **PAT recovery:** For both Qoder presets, a multi-key API-key pool can switch to another PAT on a supported authentication rejection or account-level quota exhaustion, but only before response content or an event that makes replay unsafe. Recovery requires Qoder-specific classification and respects credential-hop and send budgets; an HTTP 401/429 or quota message alone is not permission to replay. Single-key configurations do not rotate. Sharing the CLI runner does not enable this API-key recovery for CodeBuddy or self-authenticated Claude CLI.
Expand Down
6 changes: 4 additions & 2 deletions src/adapters/qoder/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ const QODER_MCP_SERVER_NAME = "opencodex";
const QODER_MCP_SERVER_PATH = fileURLToPath(new URL("../coding-agent/mcp-server.ts", import.meta.url));

export function buildQoderChildEnv(profile: QoderProfile, apiKey: string, systemPrompt?: string): Record<string, string> {
const promptEnv = profile.region === "cn" ? "QODERCN_APPEND_SYSTEM_PROMPT" : "QODER_APPEND_SYSTEM_PROMPT";
return { ...baseScopedEnv(), NO_COLOR: "1", [profile.tokenEnv]: apiKey,
// Each build reads its own brand prefix with no fallback: qodercn ignores QODER_EXPOSE_TOKEN_USAGE.
const envPrefix = profile.region === "cn" ? "QODERCN_" : "QODER_";
const promptEnv = `${envPrefix}APPEND_SYSTEM_PROMPT`;
return { ...baseScopedEnv(), NO_COLOR: "1", [`${envPrefix}EXPOSE_TOKEN_USAGE`]: "true", [profile.tokenEnv]: apiKey,
...(systemPrompt ? { [promptEnv]: systemPrompt } : {}) };
}

Expand Down
2 changes: 1 addition & 1 deletion structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ name/namespace components and wire names over 512 UTF-8 bytes before staging. It
256 KiB per serialized definition, and 2 MiB for the serialized array (including brackets and separators); the comma-joined `--allowed-tools` argument is separately capped at 8 KiB of UTF-8, counting full
`mcp__<server>__<tool>` names and commas. Invalid catalogs return a fixed local 400 without spawning Qoder. Valid wire identities, order and schemas are unchanged, apart from the Responses-only encrypted marker stripped during
projection. Duplicate and ambiguous wire identities remain the Responses parser's responsibility. Qoder appends the
folded system prompt through its documented scoped `QODER_APPEND_SYSTEM_PROMPT` or `QODERCN_APPEND_SYSTEM_PROMPT` child environment, never through command-line arguments or inherited vendor variables.
folded system prompt through its documented scoped `QODER_APPEND_SYSTEM_PROMPT` or `QODERCN_APPEND_SYSTEM_PROMPT` child environment, never through command-line arguments or inherited vendor variables. `src/adapters/qoder/adapter.ts` sets the region's usage switch (`QODER_EXPOSE_TOKEN_USAGE` or `QODERCN_EXPOSE_TOKEN_USAGE`; each CLI build reads only its own prefix) to `true` only in Qoder's child environment, so the shared usage parser receives the CLI's token counts instead of zeros.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Coding-agent stdout is framed as bounded JSONL directly from decoded stream segments. The framer
tracks the current line's UTF-8 byte count incrementally, searches each decoded segment once, and
Expand Down
19 changes: 18 additions & 1 deletion tests/providers/qoder-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,12 @@ describe("qoder adapter", () => {

test("uses only the Global PAT and disables tools, MCP, settings hooks, and persistence", () => {
const env = buildQoderChildEnv(QODER_GLOBAL_PROFILE, "qoder-pat");
expect(env.QODER_EXPOSE_TOKEN_USAGE).toBe("true");
expect(env.QODER_PERSONAL_ACCESS_TOKEN).toBe("qoder-pat");
expect(Object.keys(env).filter(key => key.startsWith("QODER"))).toEqual(["QODER_PERSONAL_ACCESS_TOKEN"]);
expect(Object.keys(env).filter(key => key.startsWith("QODER"))).toEqual([
"QODER_EXPOSE_TOKEN_USAGE",
"QODER_PERSONAL_ACCESS_TOKEN",
]);
const args = buildQoderArgs(parsed({ options: { reasoning: "high" } }), provider());
expect(args[args.indexOf("--tools") + 1]).toBe("");
expect(args[args.indexOf("--setting-sources") + 1]).toBe("");
Expand Down Expand Up @@ -496,6 +500,7 @@ describe("qoder adapter", () => {
expect(args.join(" ")).not.toContain(secretDeveloper);
expect(args).not.toContain("--append-system-prompt-file");
expect(childEnv.QODER_APPEND_SYSTEM_PROMPT).toBe(`${secretSystem}\n\n${secretDeveloper}`);
expect(childEnv.QODER_EXPOSE_TOKEN_USAGE).toBe("true");
});

test("never inherits an ambient Qoder prompt for either region", () => {
Expand Down Expand Up @@ -525,8 +530,17 @@ describe("qoder adapter", () => {

const globalEnv = buildQoderChildEnv(QODER_GLOBAL_PROFILE, "global-pat");
const cnEnv = buildQoderChildEnv(QODER_CN_PROFILE, "cn-pat");
expect(globalEnv.QODER_EXPOSE_TOKEN_USAGE).toBe("true");
expect(globalEnv.QODERCN_EXPOSE_TOKEN_USAGE).toBeUndefined();
expect(globalEnv.QODER_PERSONAL_ACCESS_TOKEN).toBe("global-pat");
expect(globalEnv.QODERCN_PERSONAL_ACCESS_TOKEN).toBeUndefined();
// The CN build derives every variable from a QODERCN_ prefix and has no QODER_ fallback for this switch.
expect(cnEnv.QODERCN_EXPOSE_TOKEN_USAGE).toBe("true");
expect(cnEnv.QODER_EXPOSE_TOKEN_USAGE).toBeUndefined();
expect(Object.keys(cnEnv).filter(key => key.startsWith("QODER"))).toEqual([
"QODERCN_EXPOSE_TOKEN_USAGE",
"QODERCN_PERSONAL_ACCESS_TOKEN",
]);
expect(cnEnv.QODERCN_PERSONAL_ACCESS_TOKEN).toBe("cn-pat");
expect(cnEnv.QODER_PERSONAL_ACCESS_TOKEN).toBeUndefined();

Expand All @@ -548,8 +562,11 @@ describe("qoder adapter", () => {
expect(spawned).toHaveLength(2);
const global = spawned.find(item => item.executable.endsWith("/qoder"))!;
const cn = spawned.find(item => item.executable.endsWith("/qodercn"))!;
expect(global.env.QODER_EXPOSE_TOKEN_USAGE).toBe("true");
expect(global.env.QODER_PERSONAL_ACCESS_TOKEN).toBe("global-pat");
expect(global.env.QODERCN_PERSONAL_ACCESS_TOKEN).toBeUndefined();
expect(cn.env.QODERCN_EXPOSE_TOKEN_USAGE).toBe("true");
expect(cn.env.QODER_EXPOSE_TOKEN_USAGE).toBeUndefined();
expect(cn.env.QODERCN_PERSONAL_ACCESS_TOKEN).toBe("cn-pat");
expect(cn.env.QODER_PERSONAL_ACCESS_TOKEN).toBeUndefined();
});
Expand Down
Loading