Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ full-slash form keeps working too. Details: [model routing docs](https://opencod
OpenAI (ChatGPT login or API key), Anthropic, Google Gemini, xAI, Kimi, Azure OpenAI, Ollama
(local + Cloud), Cursor (experimental), and every OpenAI-compatible endpoint — plus DeepSeek,
Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax,
Qwen Cloud, SiliconFlow, and more. Full list: `ocx init` or the
Qwen Cloud, Qoder Global (official PAT + CLI), SiliconFlow, and more. Full list: `ocx init` or the
[provider docs](https://opencodex.me/guides/providers/).

## CLI
Expand Down
32 changes: 32 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,38 @@ Create a key in [Novita's key manager](https://novita.ai/settings/key-management
> hosts and schemas and are not routed by this preset.
> Live discovery for this preset is capped at a 1 MiB response and 256 raw model rows.

### Official CodeBuddy Code CLI (Global & CN)

OpenCodex provides official adapter support for Tencent Cloud's CodeBuddy Code CLI via the `codebuddy` (Global) and `codebuddy-cn` (China) presets.

```json
{
"providers": {
"codebuddy": {
"adapter": "codebuddy",
"baseUrl": "https://www.codebuddy.ai",
"apiKey": "${CODEBUDDY_API_KEY}"
},
"codebuddy-cn": {
"adapter": "codebuddy",
"baseUrl": "https://www.codebuddy.cn",
"apiKey": "${CODEBUDDY_CN_API_KEY}"
}
}
}
```

- **Prerequisites:** Install the official CodeBuddy CLI globally:
```bash
npm install -g @tencent-ai/codebuddy-code
```
- **Authentication:** Obtain your official API key from the vendor console:
- Global: [CodeBuddy Global API Keys](https://www.codebuddy.ai/profile/keys)
- CN: [CodeBuddy CN API Keys](https://copilot.tencent.com/profile/keys)
- **Region Isolation:** `codebuddy` and `codebuddy-cn` use separate canonical endpoints (`https://www.codebuddy.ai` and `https://www.codebuddy.cn`) and isolated child environments (`CODEBUDDY_INTERNET_ENVIRONMENT=public` vs `internal`). Credentials are strictly region-scoped and never exchanged across environments. Overriding the canonical base URL fails closed.
- **Tool Ownership:** In v1, the CLI is spawned with `--tools ""` and `--strict-mcp-config`, ensuring Codex maintains exclusive tool ownership. The provider operates in text and reasoning mode; client tool execution is not delegated to the vendor CLI.
- **Entitlements and Billing:** The provider uses the same vendor-documented CodeBuddy account/CLI authentication surface. Availability and billing of free, promotional, trial, or subscription credits remain determined by the user's CodeBuddy account entitlement.

### A6API credit quota

A custom `openai-chat` provider using `authMode: "key"` and the canonical
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.

| Field | Type | Meaning |
| --- | --- | --- |
| `adapter` | `string` | One of `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (or alias `azure`). |
| `adapter` | `string` | One of `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (or alias `azure`), `codebuddy`. |
| `baseUrl` | `string` | Upstream API base URL. Most built-in fixed endpoints ignore a mismatch; collision-safe key presets preserve an older same-named custom destination. |
| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. Provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay. Queue waits do not consume the upstream response-header timeout. HTTP, Responses WebSocket, and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. |
| `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. |
Expand Down
43 changes: 43 additions & 0 deletions docs/qoder-cli-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Qoder CLI providers

OpenCodex supports Qoder Global through Qoder's official Personal Access Token and headless CLI.
It does not read Qoder Desktop sessions, browser cookies, refresh tokens, or private console APIs.

## Qoder Global

1. Install the official CLI: `npm install -g @qoder-ai/qodercli`.
2. Create a PAT from `https://qoder.com/account/integrations`.
3. Add the `qoder` provider in `ocx init` or the Providers workspace and paste that PAT as the API key.
4. Run `ocx provider test qoder` to verify CLI authentication and account-specific model discovery.

OpenCodex passes the stored key only as `QODER_PERSONAL_ACCESS_TOKEN` in a scoped child environment.
The adapter accepts only the canonical `https://qoder.com` destination. A legacy custom provider
named `qoder` with another destination keeps its existing adapter and URL.

The CLI is invoked in one-turn `stream-json` mode with built-in tools disabled (`--tools ""`), MCP
restricted with an empty strict configuration, setting sources disabled, and session persistence
disabled. Codex remains the only tool owner. The first version is text/reasoning only; image input
fails explicitly until the provider route has verified multimodal evidence.

`qoder --list-models` is the authoritative entitlement roster for the current PAT. OpenCodex uses
its normal model cache and credential-generation invalidation. If discovery fails, it degrades to a
stale cache and then the documented static seed. Quota totals and reset times remain unavailable
because no public quota API is used; insufficient-credit errors are still surfaced as HTTP 429.

Free, trial, promotional, and subscription credits are expected to use the account attached to the
official PAT/CLI, but the exact product eligibility is account-controlled and is not inferred by
OpenCodex. There is no automatic regional failover or credential exchange. The companion Qoder CN
integration is intentionally delivered as a separate provider/PR with its own PAT, CLI profile,
model entitlement, cache, usage, and health state.

Primary sources (verified 2026-09-03):

- Installation: <https://docs.qoder.com/cli/installation>
- PAT authentication: <https://docs.qoder.com/cli/authentication>
- Headless scripts and CI: <https://docs.qoder.com/cli/run-in-scripts>
- Account model discovery: <https://docs.qoder.com/cli/model>
- SDK/tool configuration: <https://docs.qoder.com/cli/sdk/references-typescript>
- Terms: <https://qoder.com/product-service>

The service terms identify BRIGHT ZENITH PRIVATE LIMITED as the operator. This integration uses the
documented CLI automation surface; maintainers should still make the final routing/AUP determination.
84 changes: 84 additions & 0 deletions src/adapters/codebuddy/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
import type { AdapterRequest, ProviderAdapter } from "../base";
import { mapReasoningEffort } from "../../reasoning-effort";
import { buildSystemPrompt } from "../coding-agent/protocol";
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type SpawnFn } from "../coding-agent/turn";
import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "./profiles";

export type { SpawnFn } from "../coding-agent/turn";
export type CodeBuddyAdapterDeps = CodingAgentDeps;

/**
* Build the scoped child-process environment for a CodeBuddy turn (§六/§十四).
*
* The region switch and credential are layered on top of the shared base env, which never inherits a
* parent `CODEBUDDY_*`. `CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS=1` matches the vendor SDK's own
* single-shot behavior (a `-p` turn stops at the first result and cannot receive cross-turn
* background push-back).
*/
export function buildChildEnv(profile: CodeBuddyProfile, apiKey: string): Record<string, string> {
return {
...baseScopedEnv(),
CODEBUDDY_API_KEY: apiKey,
CODEBUDDY_INTERNET_ENVIRONMENT: profile.internetEnvironment,
CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS: "1",
};
}

/**
* Build the headless CLI arguments (§七/§十一).
*
* Tool ownership stays with Codex: `--tools ""` disables every built-in tool and `--strict-mcp-config`
* (with no `--mcp-config`) blocks MCP tools, so the CLI can neither read, write, exec, nor browse the
* workspace. `-y/--dangerously-skip-permissions` is deliberately NOT passed, so any operation that
* would require authorization is blocked. The turn is a single text/reasoning pass over stream-json;
* Codex's tool catalog is not advertised in v1 (the control-protocol tool bridge is a fast-follow).
*/
export function buildArgs(profile: CodeBuddyProfile, parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] {
const args: string[] = [
"-p",
"--output-format", "stream-json",
"--input-format", "stream-json",
"--include-partial-messages",
"--verbose",
"--no-session-persistence",
"--tools", "",
"--strict-mcp-config",
"--max-turns", "1",
"--model", parsed.modelId,
];
const effort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
if (effort) args.push("--effort", effort);
const system = buildSystemPrompt(parsed);
if (system) args.push("--append-system-prompt", system);
// profile is retained for symmetry with the region-isolated design and future per-region flags.
void profile;
return args;
}

export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBuddyAdapterDeps = {}): ProviderAdapter {
return {
name: "codebuddy",

// runTurn owns the turn; buildRequest/parseStream are the disabled HTTP path (mirrors cursor).
buildRequest(): AdapterRequest {
return { url: provider.baseUrl, method: "POST", headers: {}, body: "" };
},
async *parseStream(): AsyncGenerator<AdapterEvent> {
yield { type: "error", message: "CodeBuddy adapter uses runTurn; the fetch/parseStream path is disabled." };
},

async runTurn(parsed, incoming, emit): Promise<void> {
await runCodingAgentTurn({
profiles: CODEBUDDY_PROFILES,
provider,
parsed,
incoming,
emit,
buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov),
buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
deps,
});
},
};
}
52 changes: 52 additions & 0 deletions src/adapters/codebuddy/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { clearCodingAgentBinaryCache, type CodingAgentProviderProfile } from "../coding-agent/profile";

/**
* Region-isolated profiles for the official CodeBuddy Code CLI.
*
* CodeBuddy Global and CodeBuddy CN are SEPARATE credential destinations (§五/§十四/§十六). They
* share one adapter, one binary name, and the shared coding-agent stream-json parser; the region is
* fixed by the officially documented `CODEBUDDY_INTERNET_ENVIRONMENT` value (`public` for the
* overseas/global product, `internal` for the China product) — the vendor states: "使用
* CODEBUDDY_API_KEY 时,必须根据版本正确配置 CODEBUDDY_INTERNET_ENVIRONMENT". A global key is never
* sent to the CN environment or vice versa.
*
* Evidence (verified 2026-09-03): npm `@tencent-ai/codebuddy-code` v2.143.0 (Tencent Cloud);
* keys https://www.codebuddy.ai/profile/keys (Global) / https://copilot.tencent.com/profile/keys (CN);
* headless https://www.codebuddy.ai/docs/cli/headless.
*/
export interface CodeBuddyProfile extends CodingAgentProviderProfile {
family: "codebuddy";
/** Official `CODEBUDDY_INTERNET_ENVIRONMENT` value for this region. */
internetEnvironment: "public" | "internal";
}

export const CODEBUDDY_GLOBAL_PROFILE: CodeBuddyProfile = {
providerId: "codebuddy",
family: "codebuddy",
region: "global",
label: "CodeBuddy",
internetEnvironment: "public",
canonicalBaseUrl: "https://www.codebuddy.ai",
binaryCandidates: ["codebuddy", "cbc", "codebuddy-code"],
tokenEnv: "CODEBUDDY_API_KEY",
installHint: "npm install -g @tencent-ai/codebuddy-code",
documentationUrl: "https://www.codebuddy.ai/docs/cli/headless",
};

export const CODEBUDDY_CN_PROFILE: CodeBuddyProfile = {
providerId: "codebuddy-cn",
family: "codebuddy",
region: "cn",
label: "CodeBuddy CN",
internetEnvironment: "internal",
canonicalBaseUrl: "https://www.codebuddy.cn",
binaryCandidates: ["codebuddy", "cbc", "codebuddy-code"],
tokenEnv: "CODEBUDDY_API_KEY",
installHint: "npm install -g @tencent-ai/codebuddy-code",
documentationUrl: "https://www.codebuddy.cn/docs/cli/headless",
};

export const CODEBUDDY_PROFILES: readonly CodeBuddyProfile[] = [CODEBUDDY_GLOBAL_PROFILE, CODEBUDDY_CN_PROFILE];

/** Binary-discovery cache is shared across coding-agent families; re-exported for test isolation. */
export const clearCodeBuddyBinaryCache = clearCodingAgentBinaryCache;
100 changes: 100 additions & 0 deletions src/adapters/coding-agent/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { existsSync } from "node:fs";
import { delimiter, join } from "node:path";

/**
* One region-isolated official coding-agent CLI target (§三十一).
*
* A profile is the ONLY place a family encodes its per-region differences (binary, credential env
* var, canonical destination, install hint). Adapters stay profile-driven so there is no scattered
* `if (provider === "codebuddy-cn")` branching, and so a family's Global and CN variants share one
* adapter and one parser (§十三).
*/
export interface CodingAgentProviderProfile {
/** Canonical OpenCodex provider id this profile serves. */
providerId: string;
/** Vendor family; selects the arg/env builder in the family adapter. */
family: "codebuddy" | "qoder";
/** Region; drives the vendor's own region switch and keeps credentials deterministic. */
region: "global" | "cn";
/** Human label for diagnostics/error copy (never sent upstream). */
label: string;
/**
* Canonical upstream destination and region identity. The CLI performs the real transport, but
* this host selects the profile and fails closed when overridden, so a region-scoped credential is
* never handed to an unexpected environment (§十六).
*/
canonicalBaseUrl: string;
/** Executable names to resolve on PATH, in preference order. */
binaryCandidates: readonly string[];
/** Official credential environment variable consumed by the CLI. */
tokenEnv: string;
/** Install command surfaced when the CLI is missing (§二十六). */
installHint: string;
/** Official documentation for the automation surface. */
documentationUrl: string;
}

/** Test seam: report the resolved path of a candidate executable, or undefined. */
export type WhichFn = (candidate: string) => string | undefined;

const binaryCache = new Map<string, string>();

/** Reset the discovery cache (tests, or an explicit provider re-check). */
export function clearCodingAgentBinaryCache(): void {
binaryCache.clear();
}

/** Default PATH scan: return the first existing executable path for a candidate name. */
export function whichFromPath(candidate: string): string | undefined {
const pathVar = process.env.PATH ?? "";
if (!pathVar) return undefined;
const extensions = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
for (const dir of pathVar.split(delimiter)) {
if (!dir) continue;
for (const ext of extensions) {
const full = join(dir, `${candidate}${ext}`);
try {
if (existsSync(full)) return full;
} catch {
// An unreadable PATH entry must not abort discovery; skip it.
}
}
}
return undefined;
}

/**
* Discover the CLI executable BEFORE a request is sent (§二十六), so a missing CLI is a clear
* pre-flight error rather than a mid-turn ENOENT. Only positive hits are cached (§三十): a CLI
* installed after startup is found on the next turn instead of being masked by a cached negative.
*/
export function resolveCodingAgentBinary(
profile: CodingAgentProviderProfile,
which: WhichFn = whichFromPath,
): string | undefined {
for (const candidate of profile.binaryCandidates) {
const cacheKey = `${profile.providerId}:${candidate}`;
const cached = binaryCache.get(cacheKey);
if (cached) return cached;
const resolved = which(candidate);
if (resolved) {
binaryCache.set(cacheKey, resolved);
return resolved;
}
}
return undefined;
}

/**
* Resolve the profile whose canonical base URL matches the provider's configured destination.
* Returns undefined for any other host, so the adapter fails closed rather than sending a
* region-scoped credential to an unknown environment (§十六).
*/
export function resolveProfileByBaseUrl(
profiles: readonly CodingAgentProviderProfile[],
baseUrl: string | undefined,
): CodingAgentProviderProfile | undefined {
if (!baseUrl) return undefined;
const normalized = baseUrl.replace(/\/+$/, "").toLowerCase();
return profiles.find(profile => normalized === profile.canonicalBaseUrl.toLowerCase());
}
Loading
Loading