@gajae-code/ai 0.11.11 → 0.12.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 +22 -0
- package/README.md +3 -0
- package/dist/types/provider-models/openai-compat.d.ts +7 -0
- package/dist/types/providers/anthropic.d.ts +2 -1
- package/dist/types/providers/azure-openai-responses.d.ts +6 -1
- package/dist/types/providers/google-auth.d.ts +2 -0
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +4 -0
- package/dist/types/providers/openai-completions.d.ts +2 -0
- package/dist/types/providers/openai-responses.d.ts +2 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/usage/grok-cli.d.ts +3 -1
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +8 -0
- package/dist/types/utils/foundry.d.ts +10 -0
- package/dist/types/utils/http-inspector.d.ts +13 -0
- package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-storage.ts +6 -0
- package/src/cli.ts +1 -0
- package/src/models.json +72 -0
- package/src/provider-models/descriptors.ts +7 -0
- package/src/provider-models/openai-compat.ts +67 -6
- package/src/providers/anthropic.ts +8 -4
- package/src/providers/azure-openai-responses.ts +16 -3
- package/src/providers/google-auth.ts +13 -2
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-vertex.ts +7 -2
- package/src/providers/openai-codex-responses.ts +52 -10
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-responses.ts +13 -9
- package/src/stream.ts +1 -0
- package/src/types.ts +1 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/grok-cli.ts +12 -1
- package/src/usage/kimi.ts +16 -2
- package/src/utils/anthropic-auth.ts +11 -3
- package/src/utils/foundry.ts +12 -2
- package/src/utils/http-inspector.ts +77 -0
- package/src/utils/oauth/bizrouter.ts +15 -0
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.12.0] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added first-class support for **BizRouter**, an OpenAI-compatible Korean enterprise LLM gateway. Registers the `bizrouter` provider descriptor, `/login` entry (API-key paste validated against `https://api.bizrouter.ai/v1/models`), `BIZROUTER_API_KEY` environment resolution, and bundled `models.json` seed models. Models are discovered dynamically from `GET /v1/models` (base URL `https://api.bizrouter.ai/v1`).
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Anthropic subscription OAuth requests now use the current Claude Code compatibility attribution (`2.1.219`, `sdk-cli`) instead of the stale `2.1.63` CLI fingerprint that Anthropic can misclassify as extra usage.
|
|
13
|
+
- Connection failures now name the transport code and the target URL. Bun reports DNS and socket failures as a bare `Error` whose message is a standalone hint ("Was there a typo in the url or port?", "Unable to connect. Is the computer able to access the url?") and keeps the actionable facts on `code` and `path`, but only `message` reached the assistant message. A provider outage, a local DNS failure, and a mistyped custom base URL therefore all rendered as the same context-free sentence with no host in it. Such failures now read `... (transport=FailedToOpenSocket url=https://chatgpt.com/backend-api/codex/responses)`; the URL is reduced to origin and path so a key carried in the query string is not surfaced.
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
- `docs/environment-variables.md` now names the Anthropic Foundry gateway variables that are actually read: `CLAUDE_CODE_USE_FOUNDRY`, `CLAUDE_CODE_CLIENT_CERT`, and `CLAUDE_CODE_CLIENT_KEY`. The page advertised `ANTHROPIC_MODEL_CODE_*` spellings that no code path reads, so an operator following it could not enable Foundry mode at all, and the mTLS client material was silently ignored.
|
|
18
|
+
|
|
5
19
|
## [0.11.11] - 2026-07-26
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
8
22
|
|
|
23
|
+
- The Kimi usage endpoint base (`KIMI_CODE_BASE_URL`) is now resolved from trusted environment sources only. That base becomes the URL the usage request sends `Authorization: Bearer <accessToken>` to, so reading it through the merged view that includes the caller's `cwd/.env` let a repository collect the user's Kimi access token. An explicit caller-supplied base URL still takes precedence, and shell / user-level configuration is unchanged.
|
|
24
|
+
- The Gemini CLI compatibility version used in the outbound `User-Agent` is refreshed from `0.50.0` to `0.52.0`, matching the current upstream release. The repository ships `check-spoofed-versions` for exactly this, but that check is not wired into CI, so the value had drifted two minor releases behind.
|
|
25
|
+
- The OpenAI and Azure endpoint decisions are now resolved from trusted environment sources only: `OPENAI_BASE_URL` (streaming responses, completions, and the model manager) and `AZURE_OPENAI_BASE_URL`. `Bun.env` is `process.env` and the env module merges the caller's `cwd/.env` into it, so a repository could previously plant a `.env` that redirected authenticated requests; the two provider paths already reached for `$inheritedEnv` but re-admitted the project `.env` through a fallback. Resolution now goes through the non-project resolver (launching shell plus GJC/user-owned `.env` files); shell and user-level configuration is unchanged.
|
|
26
|
+
- The OpenAI and Azure endpoint decisions are now resolved from trusted environment sources only: `OPENAI_BASE_URL` (streaming responses, completions, and the model manager), `AZURE_OPENAI_BASE_URL`, and `AZURE_OPENAI_RESOURCE_NAME` (the alternate constructor for the same Azure host). `Bun.env` is `process.env` and the env module merges the caller's `cwd/.env` into it, so a repository could previously plant a `.env` that redirected authenticated requests; the two provider paths already reached for `$inheritedEnv` but re-admitted the project `.env` through a fallback. Resolution now goes through the non-project resolver (launching shell plus GJC/user-owned `.env` files); shell and user-level configuration is unchanged.
|
|
27
|
+
- Google credential material is now resolved from trusted environment sources only: the `GOOGLE_APPLICATION_CREDENTIALS` service-account / authorized-user file path used by the ADC loader, and `GOOGLE_CLOUD_API_KEY` used as the Vertex API key. Both were read through the merged view that includes the caller's `cwd/.env`, so a repository could ship a key file and point the agent at it, making it authenticate to Google as an identity the repository chose. `stream.ts` already resolved the same ADC variable through the non-project resolver; the two now agree. An explicit caller-supplied API key still takes precedence.
|
|
28
|
+
- The Grok usage token fallback (`GROK_CLI_OAUTH_TOKEN`) is now resolved from trusted environment sources only. It authenticates the billing/usage call, and reading it through the merged view that includes the caller's `cwd/.env` let a repository decide which account that call ran against. Stored credentials keep precedence, and shell / user-level configuration is unchanged.
|
|
9
29
|
- Anthropic `ping` keepalives no longer reset stream progress, so responses that stop producing content now reach the idle timeout instead of hanging indefinitely.
|
|
30
|
+
- The Anthropic endpoint decision is now resolved from trusted environment sources only: `ANTHROPIC_BASE_URL`, `FOUNDRY_BASE_URL`, `ZCODE_PLAN_ANTHROPIC_BASE_URL`, and the `CLAUDE_CODE_USE_FOUNDRY` mode switch. `Bun.env` is `process.env` and the env module merges the caller's `cwd/.env` into it, so a repository could previously plant a `.env` that redirected authenticated Anthropic requests — the resolved base URL becomes `${baseUrl}/v1/messages` while the headers carry the API key or OAuth token. Resolution now goes through the non-project resolver (launching shell plus GJC/user-owned `.env` files); shell and user-level configuration is unchanged.
|
|
10
31
|
- The documented `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` environment variable now takes effect: the stream-watchdog idle-timeout helpers resolve it GJC-first before the legacy `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` / `PI_STREAM_IDLE_TIMEOUT_MS` aliases (previously only the `PI_`-prefixed names were read, so setting the documented GJC name was a silent no-op).
|
|
32
|
+
- The documented OpenAI-code provider knobs now take effect: `GJC_OPENAI_CODE_DEBUG`, `GJC_OPENAI_CODE_WEBSOCKET`, `GJC_OPENAI_CODE_WEBSOCKET_IDLE_TIMEOUT_MS`, `GJC_OPENAI_CODE_WEBSOCKET_RETRY_BUDGET`, and `GJC_OPENAI_CODE_WEBSOCKET_RETRY_DELAY_MS` are resolved GJC-first ahead of the legacy `PI_CODEX_*` names. The Codex → OpenAI-code rename had updated the documentation but not the reads, so every documented name was a silent no-op.
|
|
11
33
|
|
|
12
34
|
## [0.11.9] - 2026-07-24
|
|
13
35
|
### Fixed
|
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
|
|
70
70
|
- **Xiaomi MiMo** (requires `XIAOMI_API_KEY`)
|
|
71
71
|
- **ZenMux** (requires `ZENMUX_API_KEY`)
|
|
72
72
|
- **OpenGateway by Sionic AI** (requires `OPENGATEWAY_API_KEY`)
|
|
73
|
+
- **BizRouter** (requires `BIZROUTER_API_KEY`)
|
|
73
74
|
- **Qwen Portal** (supports `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`)
|
|
74
75
|
- **Cloudflare AI Gateway** (requires `CLOUDFLARE_AI_GATEWAY_API_KEY` and provider-specific gateway base URL)
|
|
75
76
|
- **Ollama** (local OpenAI-compatible runtime; optional `OLLAMA_API_KEY`)
|
|
@@ -956,6 +957,7 @@ In Node.js environments, you can set environment variables to avoid passing API
|
|
|
956
957
|
| Xiaomi MiMo | `XIAOMI_API_KEY` |
|
|
957
958
|
| ZenMux | `ZENMUX_API_KEY` |
|
|
958
959
|
| OpenGateway | `OPENGATEWAY_API_KEY` |
|
|
960
|
+
| BizRouter | `BIZROUTER_API_KEY` |
|
|
959
961
|
| vLLM | `VLLM_API_KEY` |
|
|
960
962
|
| Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` |
|
|
961
963
|
| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |
|
|
@@ -980,6 +982,7 @@ Provider endpoint defaults for the current OpenAI-compatible integrations:
|
|
|
980
982
|
- ZenMux (OpenAI): `https://zenmux.ai/api/v1`
|
|
981
983
|
- ZenMux (Anthropic models): `https://zenmux.ai/api/anthropic`
|
|
982
984
|
- OpenGateway by Sionic AI: `https://apis.opengateway.ai/v1`
|
|
985
|
+
- BizRouter: `https://api.bizrouter.ai/v1`
|
|
983
986
|
- vLLM: `http://127.0.0.1:8000/v1`
|
|
984
987
|
- Ollama: local OpenAI-compatible runtime (`http://127.0.0.1:11434/v1`)
|
|
985
988
|
- Ollama Cloud: native Ollama API host (`https://ollama.com/api`, configured here as base URL `https://ollama.com`)
|
|
@@ -27,6 +27,8 @@ export interface OpenAIModelManagerConfig {
|
|
|
27
27
|
apiKey?: string;
|
|
28
28
|
baseUrl?: string;
|
|
29
29
|
}
|
|
30
|
+
/** Test seam: the model-manager base URL as resolved from trusted env. */
|
|
31
|
+
export declare function resolveOpenAIModelManagerBaseUrlForTest(config?: OpenAIModelManagerConfig): string;
|
|
30
32
|
export declare function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses">;
|
|
31
33
|
export interface GroqModelManagerConfig {
|
|
32
34
|
apiKey?: string;
|
|
@@ -121,6 +123,11 @@ export interface OpenGatewayModelManagerConfig {
|
|
|
121
123
|
* the OpenAI-compatible `/v1/models` endpoint.
|
|
122
124
|
*/
|
|
123
125
|
export declare function opengatewayModelManagerOptions(config?: OpenGatewayModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
126
|
+
export interface BizRouterModelManagerConfig {
|
|
127
|
+
apiKey?: string;
|
|
128
|
+
baseUrl?: string;
|
|
129
|
+
}
|
|
130
|
+
export declare function bizrouterModelManagerOptions(config?: BizRouterModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
124
131
|
export interface KiloModelManagerConfig {
|
|
125
132
|
apiKey?: string;
|
|
126
133
|
baseUrl?: string;
|
|
@@ -50,7 +50,8 @@ export declare function isAnthropicThinkingBlockMutationError(error: unknown): b
|
|
|
50
50
|
* than only the latest one.
|
|
51
51
|
*/
|
|
52
52
|
export declare function isAnthropicThinkingSignatureInvalidError(error: unknown): boolean;
|
|
53
|
-
export declare const claudeCodeVersion = "2.1.
|
|
53
|
+
export declare const claudeCodeVersion = "2.1.219";
|
|
54
|
+
export declare const claudeCodeEntrypoint = "sdk-cli";
|
|
54
55
|
export declare const claudeToolPrefix: string;
|
|
55
56
|
export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
56
57
|
export declare function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServiceTier, StreamFunction, StreamOptions, ToolChoice } from "../types";
|
|
1
|
+
import type { Model, ServiceTier, StreamFunction, StreamOptions, ToolChoice } from "../types";
|
|
2
2
|
export interface AzureOpenAIResponsesOptions extends StreamOptions {
|
|
3
3
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
4
4
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
@@ -13,3 +13,8 @@ export interface AzureOpenAIResponsesOptions extends StreamOptions {
|
|
|
13
13
|
* Generate function for Azure OpenAI Responses API
|
|
14
14
|
*/
|
|
15
15
|
export declare const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses">;
|
|
16
|
+
/** Test seam: the Azure endpoint config as resolved from trusted env. */
|
|
17
|
+
export declare function resolveAzureConfigForTest(model: Model<"azure-openai-responses">, options?: AzureOpenAIResponsesOptions): {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
apiVersion: string;
|
|
20
|
+
};
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* (default 60s). Concurrent callers waiting on a refresh share the same in-flight promise.
|
|
13
13
|
*/
|
|
14
14
|
import type { FetchImpl } from "../types";
|
|
15
|
+
/** Test seam: the ADC credentials file path as resolved from trusted env. */
|
|
16
|
+
export declare function resolveAdcCredentialsPathForTest(): string | undefined;
|
|
15
17
|
/**
|
|
16
18
|
* Returns a Bearer access token suitable for the `Authorization` header on Vertex AI calls.
|
|
17
19
|
* The token is cached in module scope and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` ms before it expires.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
|
|
7
7
|
export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
|
|
8
|
-
export declare const DEFAULT_GEMINI_CLI_VERSION = "0.
|
|
8
|
+
export declare const DEFAULT_GEMINI_CLI_VERSION = "0.52.0";
|
|
9
9
|
export declare function getGeminiCliUserAgent(modelId?: string): string;
|
|
10
10
|
export declare const getGeminiCliHeaders: (modelId?: string) => {
|
|
11
11
|
"User-Agent": string;
|
|
@@ -5,3 +5,5 @@ export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
|
|
|
5
5
|
location?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare const streamGoogleVertex: StreamFunction<"google-vertex">;
|
|
8
|
+
/** Test seam: the Vertex API key as resolved from options plus trusted env. */
|
|
9
|
+
export declare function resolveVertexApiKeyForTest(options?: GoogleVertexOptions): string | undefined;
|
|
@@ -10,6 +10,10 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
10
10
|
preferWebsockets?: boolean;
|
|
11
11
|
serviceTier?: ServiceTier;
|
|
12
12
|
}
|
|
13
|
+
/** Maps a canonical tool name to the name Codex accepts on the wire. */
|
|
14
|
+
export declare function codexToolWireName(name: string): string;
|
|
15
|
+
/** Maps a Codex wire tool name back to the canonical harness tool name. */
|
|
16
|
+
export declare function codexToolCanonicalName(wireName: string): string;
|
|
13
17
|
type CodexTransport = "sse" | "websocket";
|
|
14
18
|
export interface OpenAICodexWebSocketDebugStats {
|
|
15
19
|
fullContextRequests: number;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
|
|
2
2
|
import { type AssistantMessage, type Context, type Model, type ServiceTier, type StreamFunction, type StreamOptions, type ToolChoice } from "../types";
|
|
3
3
|
import { type ResolvedOpenAICompat } from "./openai-completions-compat";
|
|
4
|
+
/** Test seam: the provider base URL as resolved from trusted env. */
|
|
5
|
+
export declare function resolveOpenAICompletionsBaseUrlForTest(baseUrl: string | undefined, authCredentialType: "api_key" | "oauth" | undefined): string;
|
|
4
6
|
/**
|
|
5
7
|
* Identify "real progress" stream chunks vs. keepalives, role-only preambles,
|
|
6
8
|
* and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
|
|
@@ -13,6 +13,8 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
13
13
|
*/
|
|
14
14
|
strictResponsesPairing?: boolean;
|
|
15
15
|
}
|
|
16
|
+
/** Test seam: the provider base URL as resolved from trusted env. */
|
|
17
|
+
export declare function resolveOpenAIProviderBaseUrlForTest(baseUrl: string | undefined, authCredentialType: "api_key" | "oauth" | undefined): string;
|
|
16
18
|
/**
|
|
17
19
|
* Generate function for OpenAI Responses API
|
|
18
20
|
*/
|
package/dist/types/types.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ThinkingConfig {
|
|
|
51
51
|
/** Provider-specific transport used to encode the selected effort. */
|
|
52
52
|
mode: ThinkingControlMode;
|
|
53
53
|
}
|
|
54
|
-
export type KnownProvider = "alibaba-token-plan" | "amazon-bedrock" | "azure-openai" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "fugu" | "gitlab-duo" | "cursor" | "deepseek" | "deepinfra" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "glm-zcode" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "opengateway" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "lm-studio";
|
|
54
|
+
export type KnownProvider = "alibaba-token-plan" | "amazon-bedrock" | "azure-openai" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "fugu" | "gitlab-duo" | "cursor" | "deepseek" | "deepinfra" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "glm-zcode" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "opengateway" | "bizrouter" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "lm-studio";
|
|
55
55
|
export type Provider = KnownProvider | string;
|
|
56
56
|
import type { Effort } from "./model-thinking";
|
|
57
57
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { CredentialRankingStrategy, UsageProvider } from "../usage";
|
|
1
|
+
import type { CredentialRankingStrategy, UsageFetchParams, UsageProvider } from "../usage";
|
|
2
2
|
interface BillingUsage {
|
|
3
3
|
monthlyLimit: number;
|
|
4
4
|
used: number;
|
|
5
5
|
billingPeriodEnd: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function parseGrokCliBillingUsage(payload: unknown): BillingUsage;
|
|
8
|
+
/** Test seam: the usage access token as resolved from a credential plus trusted env. */
|
|
9
|
+
export declare function resolveGrokAccessTokenForTest(params: UsageFetchParams): string | undefined;
|
|
8
10
|
export declare const grokCliUsageProvider: UsageProvider;
|
|
9
11
|
export declare const grokCliRankingStrategy: CredentialRankingStrategy;
|
|
10
12
|
export {};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { UsageProvider } from "../usage";
|
|
2
|
+
/** Test seam: the usage base URL as resolved from a caller value plus trusted env. */
|
|
3
|
+
export declare function normalizeKimiUsageBaseUrlForTest(baseUrl?: string): string;
|
|
2
4
|
export declare const kimiUsageProvider: UsageProvider;
|
|
@@ -4,6 +4,14 @@ export interface AnthropicAuthConfig {
|
|
|
4
4
|
baseUrl: string;
|
|
5
5
|
isOAuth: boolean;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the Anthropic base URL from the environment.
|
|
9
|
+
*
|
|
10
|
+
* Trusted sources only: the result becomes the request URL that carries the
|
|
11
|
+
* Anthropic API key / OAuth token, so whatever can set it can redirect
|
|
12
|
+
* authenticated traffic. `$env` merges the caller's `cwd/.env`, so reading it
|
|
13
|
+
* there would let repository content choose where credentials are sent.
|
|
14
|
+
*/
|
|
7
15
|
export declare function resolveAnthropicBaseUrlFromEnv(): string | undefined;
|
|
8
16
|
/**
|
|
9
17
|
* Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
|
|
@@ -1 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Anthropic requests run in Foundry gateway mode.
|
|
3
|
+
*
|
|
4
|
+
* Resolved from trusted environment sources only. Enabling Foundry switches the
|
|
5
|
+
* request base URL and injects TLS client material, so whatever can set this
|
|
6
|
+
* redirects authenticated traffic. `$env` merges the caller's `cwd/.env`, so
|
|
7
|
+
* reading it there would let repository content flip the mode; resolve it the
|
|
8
|
+
* same way the credentials themselves are (launching shell plus GJC/user-owned
|
|
9
|
+
* `.env` files, never the project `.env`).
|
|
10
|
+
*/
|
|
1
11
|
export declare function isFoundryEnabled(): boolean;
|
|
@@ -18,6 +18,19 @@ export declare function isModelUnavailableError(message: string, error: unknown)
|
|
|
18
18
|
/** Actionable guidance for selecting an available model/provider. */
|
|
19
19
|
export declare function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefined): string;
|
|
20
20
|
export declare function appendRawHttpRequestDumpFor400(message: string, error: unknown, dump: RawHttpRequestDump | undefined): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Name the failed connection when the request never produced an HTTP status.
|
|
23
|
+
*
|
|
24
|
+
* Bun raises DNS and socket failures as a bare `Error` whose message is a
|
|
25
|
+
* standalone hint ("Was there a typo in the url or port?", "Unable to connect.
|
|
26
|
+
* Is the computer able to access the url?") while the actionable facts live on
|
|
27
|
+
* `code` and `path`. Those properties are dropped when only `message` reaches
|
|
28
|
+
* the assistant message, so a provider outage, a local DNS failure, and a
|
|
29
|
+
* mistyped custom base URL all render as the same context-free sentence.
|
|
30
|
+
* Appending the code and the target URL tells the user which host failed and
|
|
31
|
+
* whether the fault is theirs.
|
|
32
|
+
*/
|
|
33
|
+
export declare function appendTransportFailureContext(message: string, error: unknown, rawRequestDump: RawHttpRequestDump | undefined): string;
|
|
21
34
|
export declare function finalizeErrorMessage(error: unknown, rawRequestDump: RawHttpRequestDump | undefined, capturedErrorResponse?: CapturedHttpErrorResponse): Promise<string>;
|
|
22
35
|
export declare function withHttpStatus(error: unknown, status: number): Error;
|
|
23
36
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const loginBizRouter: (options: import("./types").OAuthController) => Promise<string>;
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "alibaba-token-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai";
|
|
10
|
+
export type OAuthProvider = "alibaba-token-plan" | "anthropic" | "bizrouter" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/utils": "0.
|
|
43
|
+
"@gajae-code/utils": "0.12.0",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -1998,6 +1998,12 @@ export class AuthStorage {
|
|
|
1998
1998
|
await saveApiKeyCredential(apiKey);
|
|
1999
1999
|
return;
|
|
2000
2000
|
}
|
|
2001
|
+
case "bizrouter": {
|
|
2002
|
+
const { loginBizRouter } = await import("./utils/oauth/bizrouter");
|
|
2003
|
+
const apiKey = await loginBizRouter(ctrl);
|
|
2004
|
+
await saveApiKeyCredential(apiKey);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2001
2007
|
case "opengateway": {
|
|
2002
2008
|
const { loginOpenGateway } = await import("./utils/oauth/opengateway");
|
|
2003
2009
|
const apiKey = await loginOpenGateway(ctrl);
|
package/src/cli.ts
CHANGED
package/src/models.json
CHANGED
|
@@ -4030,6 +4030,78 @@
|
|
|
4030
4030
|
}
|
|
4031
4031
|
}
|
|
4032
4032
|
},
|
|
4033
|
+
"bizrouter": {
|
|
4034
|
+
"anthropic/claude-sonnet-4.5": {
|
|
4035
|
+
"id": "anthropic/claude-sonnet-4.5",
|
|
4036
|
+
"name": "Anthropic Sonnet 4.5",
|
|
4037
|
+
"api": "openai-completions",
|
|
4038
|
+
"provider": "bizrouter",
|
|
4039
|
+
"baseUrl": "https://api.bizrouter.ai/v1",
|
|
4040
|
+
"reasoning": true,
|
|
4041
|
+
"input": [
|
|
4042
|
+
"text",
|
|
4043
|
+
"image"
|
|
4044
|
+
],
|
|
4045
|
+
"cost": {
|
|
4046
|
+
"input": 3,
|
|
4047
|
+
"output": 15,
|
|
4048
|
+
"cacheRead": 0.3,
|
|
4049
|
+
"cacheWrite": 3.75
|
|
4050
|
+
},
|
|
4051
|
+
"contextWindow": 200000,
|
|
4052
|
+
"maxTokens": 64000,
|
|
4053
|
+
"thinking": {
|
|
4054
|
+
"mode": "effort",
|
|
4055
|
+
"minLevel": "minimal",
|
|
4056
|
+
"maxLevel": "xhigh"
|
|
4057
|
+
}
|
|
4058
|
+
},
|
|
4059
|
+
"google/gemini-2.5-pro": {
|
|
4060
|
+
"id": "google/gemini-2.5-pro",
|
|
4061
|
+
"name": "Gemini 2.5 Pro",
|
|
4062
|
+
"api": "openai-completions",
|
|
4063
|
+
"provider": "bizrouter",
|
|
4064
|
+
"baseUrl": "https://api.bizrouter.ai/v1",
|
|
4065
|
+
"reasoning": true,
|
|
4066
|
+
"input": [
|
|
4067
|
+
"text",
|
|
4068
|
+
"image"
|
|
4069
|
+
],
|
|
4070
|
+
"cost": {
|
|
4071
|
+
"input": 1.25,
|
|
4072
|
+
"output": 10,
|
|
4073
|
+
"cacheRead": 0.31,
|
|
4074
|
+
"cacheWrite": 0
|
|
4075
|
+
},
|
|
4076
|
+
"contextWindow": 1048576,
|
|
4077
|
+
"maxTokens": 65536,
|
|
4078
|
+
"thinking": {
|
|
4079
|
+
"mode": "effort",
|
|
4080
|
+
"minLevel": "minimal",
|
|
4081
|
+
"maxLevel": "high"
|
|
4082
|
+
}
|
|
4083
|
+
},
|
|
4084
|
+
"openai/gpt-4o": {
|
|
4085
|
+
"id": "openai/gpt-4o",
|
|
4086
|
+
"name": "GPT-4o",
|
|
4087
|
+
"api": "openai-completions",
|
|
4088
|
+
"provider": "bizrouter",
|
|
4089
|
+
"baseUrl": "https://api.bizrouter.ai/v1",
|
|
4090
|
+
"reasoning": false,
|
|
4091
|
+
"input": [
|
|
4092
|
+
"text",
|
|
4093
|
+
"image"
|
|
4094
|
+
],
|
|
4095
|
+
"cost": {
|
|
4096
|
+
"input": 2.5,
|
|
4097
|
+
"output": 10,
|
|
4098
|
+
"cacheRead": 1.25,
|
|
4099
|
+
"cacheWrite": 0
|
|
4100
|
+
},
|
|
4101
|
+
"contextWindow": 128000,
|
|
4102
|
+
"maxTokens": 16384
|
|
4103
|
+
}
|
|
4104
|
+
},
|
|
4033
4105
|
"cerebras": {
|
|
4034
4106
|
"gemma-4-31b": {
|
|
4035
4107
|
"id": "gemma-4-31b",
|
|
@@ -11,6 +11,7 @@ import { ollamaCloudModelManagerOptions } from "./ollama";
|
|
|
11
11
|
import {
|
|
12
12
|
alibabaTokenPlanModelManagerOptions,
|
|
13
13
|
anthropicModelManagerOptions,
|
|
14
|
+
bizrouterModelManagerOptions,
|
|
14
15
|
cerebrasModelManagerOptions,
|
|
15
16
|
cloudflareAiGatewayModelManagerOptions,
|
|
16
17
|
deepinfraModelManagerOptions,
|
|
@@ -319,6 +320,12 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
319
320
|
config => opengatewayModelManagerOptions(config),
|
|
320
321
|
catalog("OpenGateway by Sionic AI", ["OPENGATEWAY_API_KEY"]),
|
|
321
322
|
),
|
|
323
|
+
catalogDescriptor(
|
|
324
|
+
"bizrouter",
|
|
325
|
+
"anthropic/claude-sonnet-4.5",
|
|
326
|
+
config => bizrouterModelManagerOptions(config),
|
|
327
|
+
catalog("BizRouter", ["BIZROUTER_API_KEY"]),
|
|
328
|
+
),
|
|
322
329
|
catalogDescriptor("zai", "glm-5.2", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
323
330
|
catalogDescriptor(
|
|
324
331
|
"glm-zcode",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $credentialEnv } from "@gajae-code/utils";
|
|
2
2
|
import type { ModelManagerOptions } from "../model-manager";
|
|
3
3
|
import { Effort } from "../model-thinking";
|
|
4
4
|
import { getBundledModels } from "../models";
|
|
@@ -553,13 +553,19 @@ export interface OpenAIModelManagerConfig {
|
|
|
553
553
|
baseUrl?: string;
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
/** Base URL for the OpenAI model manager, from trusted env only (`$env` merges the caller's `cwd/.env`). */
|
|
557
|
+
function resolveOpenAIModelManagerBaseUrl(config?: OpenAIModelManagerConfig): string {
|
|
558
|
+
return config?.baseUrl?.trim() || $credentialEnv("OPENAI_BASE_URL") || OPENAI_DEFAULT_BASE_URL;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Test seam: the model-manager base URL as resolved from trusted env. */
|
|
562
|
+
export function resolveOpenAIModelManagerBaseUrlForTest(config?: OpenAIModelManagerConfig): string {
|
|
563
|
+
return resolveOpenAIModelManagerBaseUrl(config);
|
|
564
|
+
}
|
|
565
|
+
|
|
556
566
|
export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
557
567
|
const apiKey = config?.apiKey;
|
|
558
|
-
const baseUrl =
|
|
559
|
-
config?.baseUrl?.trim() ||
|
|
560
|
-
$inheritedEnv("OPENAI_BASE_URL") ||
|
|
561
|
-
$env.OPENAI_BASE_URL?.trim() ||
|
|
562
|
-
OPENAI_DEFAULT_BASE_URL;
|
|
568
|
+
const baseUrl = resolveOpenAIModelManagerBaseUrl(config);
|
|
563
569
|
const references = createBundledReferenceMap<"openai-responses">("openai");
|
|
564
570
|
return {
|
|
565
571
|
providerId: "openai",
|
|
@@ -1119,6 +1125,61 @@ export function opengatewayModelManagerOptions(
|
|
|
1119
1125
|
return createSimpleOpenAICompletionsOptions("opengateway", "https://apis.opengateway.ai/v1", config);
|
|
1120
1126
|
}
|
|
1121
1127
|
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
// 10.5.2 BizRouter
|
|
1130
|
+
// ---------------------------------------------------------------------------
|
|
1131
|
+
|
|
1132
|
+
const BIZROUTER_BASE_URL = "https://api.bizrouter.ai/v1";
|
|
1133
|
+
|
|
1134
|
+
function toBizRouterPrice(value: unknown, fallback: number): number {
|
|
1135
|
+
const parsed = toNumber(value);
|
|
1136
|
+
return parsed === undefined || parsed < 0 ? fallback : parsed;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export interface BizRouterModelManagerConfig {
|
|
1140
|
+
apiKey?: string;
|
|
1141
|
+
baseUrl?: string;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export function bizrouterModelManagerOptions(
|
|
1145
|
+
config?: BizRouterModelManagerConfig,
|
|
1146
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1147
|
+
const apiKey = config?.apiKey;
|
|
1148
|
+
const baseUrl = config?.baseUrl ?? BIZROUTER_BASE_URL;
|
|
1149
|
+
const references = createBundledReferenceMap<"openai-completions">("bizrouter");
|
|
1150
|
+
return {
|
|
1151
|
+
providerId: "bizrouter",
|
|
1152
|
+
...(apiKey && {
|
|
1153
|
+
fetchDynamicModels: () =>
|
|
1154
|
+
fetchOpenAICompatibleModels({
|
|
1155
|
+
api: "openai-completions",
|
|
1156
|
+
provider: "bizrouter",
|
|
1157
|
+
baseUrl,
|
|
1158
|
+
apiKey,
|
|
1159
|
+
mapModel: (entry, defaults) => {
|
|
1160
|
+
const mapped = mapWithBundledReference(entry, defaults, references.get(defaults.id));
|
|
1161
|
+
return {
|
|
1162
|
+
...mapped,
|
|
1163
|
+
name: toModelName(entry.display_name, mapped.name),
|
|
1164
|
+
contextWindow: toPositiveNumber(entry.context_length, mapped.contextWindow),
|
|
1165
|
+
maxTokens: toPositiveNumber(entry.max_output_tokens, mapped.maxTokens),
|
|
1166
|
+
input: toInputCapabilities(entry.input_modalities),
|
|
1167
|
+
cost: {
|
|
1168
|
+
input: toBizRouterPrice(entry.input_price_per_1m_usd, mapped.cost.input),
|
|
1169
|
+
output: toBizRouterPrice(entry.output_price_per_1m_usd, mapped.cost.output),
|
|
1170
|
+
cacheRead: mapped.cost.cacheRead,
|
|
1171
|
+
cacheWrite: mapped.cost.cacheWrite,
|
|
1172
|
+
},
|
|
1173
|
+
api: "openai-completions",
|
|
1174
|
+
provider: "bizrouter",
|
|
1175
|
+
baseUrl,
|
|
1176
|
+
};
|
|
1177
|
+
},
|
|
1178
|
+
}),
|
|
1179
|
+
}),
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1122
1183
|
// ---------------------------------------------------------------------------
|
|
1123
1184
|
// 10.6 Kilo Gateway
|
|
1124
1185
|
// ---------------------------------------------------------------------------
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RawMessageStreamEvent,
|
|
12
12
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
13
13
|
import {
|
|
14
|
+
$credentialEnv,
|
|
14
15
|
$env,
|
|
15
16
|
extractHttpStatusFromError,
|
|
16
17
|
isEnoent,
|
|
@@ -490,7 +491,8 @@ function getCacheControl(
|
|
|
490
491
|
}
|
|
491
492
|
|
|
492
493
|
// Stealth mode: Mimic Anthropic Code headers and tool prefixing.
|
|
493
|
-
export const claudeCodeVersion = "2.1.
|
|
494
|
+
export const claudeCodeVersion = "2.1.219";
|
|
495
|
+
export const claudeCodeEntrypoint = "sdk-cli";
|
|
494
496
|
export const claudeToolPrefix: string = "proxy_";
|
|
495
497
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
496
498
|
|
|
@@ -566,7 +568,7 @@ function createClaudeBillingHeader(payload: unknown): string {
|
|
|
566
568
|
const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
|
|
567
569
|
.join("")
|
|
568
570
|
.slice(0, 3);
|
|
569
|
-
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint
|
|
571
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=${claudeCodeEntrypoint}; cch=${cch};`;
|
|
570
572
|
}
|
|
571
573
|
|
|
572
574
|
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
@@ -816,10 +818,12 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
|
|
|
816
818
|
// calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
|
|
817
819
|
// discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
|
|
818
820
|
if (model.provider === "glm-zcode") {
|
|
819
|
-
return
|
|
821
|
+
return (
|
|
822
|
+
normalizeAnthropicBaseUrl($credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")) ?? "https://api.z.ai/api/anthropic"
|
|
823
|
+
);
|
|
820
824
|
}
|
|
821
825
|
if (model.provider === "anthropic" && isFoundryEnabled()) {
|
|
822
|
-
const foundryBaseUrl = normalizeAnthropicBaseUrl($
|
|
826
|
+
const foundryBaseUrl = normalizeAnthropicBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
|
|
823
827
|
if (foundryBaseUrl) {
|
|
824
828
|
return foundryBaseUrl;
|
|
825
829
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
|
|
1
|
+
import { $credentialEnv, $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
|
|
2
2
|
import { AzureOpenAI } from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
Tool as OpenAITool,
|
|
@@ -234,8 +234,13 @@ function resolveAzureConfig(
|
|
|
234
234
|
): { baseUrl: string; apiVersion: string } {
|
|
235
235
|
const apiVersion = options?.azureApiVersion || $env.AZURE_OPENAI_API_VERSION || DEFAULT_AZURE_API_VERSION;
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
// Trusted sources only: both of these decide the request endpoint that carries
|
|
238
|
+
// the Azure credential, and `$env` merges the caller's `cwd/.env`. The resource
|
|
239
|
+
// name is the alternate constructor for the same host
|
|
240
|
+
// (`https://<resource>.openai.azure.com/openai/v1`), so it needs the same
|
|
241
|
+
// boundary as the explicit base URL.
|
|
242
|
+
const baseUrl = options?.azureBaseUrl?.trim() || $credentialEnv("AZURE_OPENAI_BASE_URL") || undefined;
|
|
243
|
+
const resourceName = options?.azureResourceName || $credentialEnv("AZURE_OPENAI_RESOURCE_NAME");
|
|
239
244
|
|
|
240
245
|
let resolvedBaseUrl = baseUrl;
|
|
241
246
|
|
|
@@ -259,6 +264,14 @@ function resolveAzureConfig(
|
|
|
259
264
|
};
|
|
260
265
|
}
|
|
261
266
|
|
|
267
|
+
/** Test seam: the Azure endpoint config as resolved from trusted env. */
|
|
268
|
+
export function resolveAzureConfigForTest(
|
|
269
|
+
model: Model<"azure-openai-responses">,
|
|
270
|
+
options?: AzureOpenAIResponsesOptions,
|
|
271
|
+
): { baseUrl: string; apiVersion: string } {
|
|
272
|
+
return resolveAzureConfig(model, options);
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) {
|
|
263
276
|
if (!apiKey) {
|
|
264
277
|
const envKey = $env.AZURE_OPENAI_API_KEY;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { Buffer } from "node:buffer";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import { $envpos, isEnoent, logger } from "@gajae-code/utils";
|
|
18
|
+
import { $credentialEnv, $envpos, isEnoent, logger } from "@gajae-code/utils";
|
|
19
19
|
import type { FetchImpl } from "../types";
|
|
20
20
|
|
|
21
21
|
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -70,8 +70,19 @@ async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Test seam: the ADC credentials file path as resolved from trusted env. */
|
|
74
|
+
export function resolveAdcCredentialsPathForTest(): string | undefined {
|
|
75
|
+
return $credentialEnv("GOOGLE_APPLICATION_CREDENTIALS");
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
async function loadAdcCredentials(): Promise<{ source: string; creds: AdcFileCredentials } | undefined> {
|
|
74
|
-
|
|
79
|
+
// Trusted sources only: this path is read as service-account / authorized-user
|
|
80
|
+
// credentials and exchanged for a Google access token, so whatever can set it
|
|
81
|
+
// chooses the identity the agent authenticates as. `Bun.env` is `process.env`
|
|
82
|
+
// and the env module merges the caller's `cwd/.env` into it, so reading it
|
|
83
|
+
// there would let repository content point this at a key file it ships.
|
|
84
|
+
// `stream.ts` already resolves the same variable through `$credentialEnv`.
|
|
85
|
+
const gacPath = $credentialEnv("GOOGLE_APPLICATION_CREDENTIALS");
|
|
75
86
|
if (gacPath) {
|
|
76
87
|
const creds = await readJsonFile<AdcFileCredentials>(gacPath);
|
|
77
88
|
if (!creds) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
|
|
7
7
|
export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
|
|
8
|
-
export const DEFAULT_GEMINI_CLI_VERSION = "0.
|
|
8
|
+
export const DEFAULT_GEMINI_CLI_VERSION = "0.52.0";
|
|
9
9
|
|
|
10
10
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
11
11
|
const version =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env } from "@gajae-code/utils";
|
|
1
|
+
import { $credentialEnv, $env } from "@gajae-code/utils";
|
|
2
2
|
import type { Context, Model, StreamFunction } from "../types";
|
|
3
3
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
4
4
|
import { getVertexAccessToken } from "./google-auth";
|
|
@@ -58,12 +58,17 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
/** Test seam: the Vertex API key as resolved from options plus trusted env. */
|
|
62
|
+
export function resolveVertexApiKeyForTest(options?: GoogleVertexOptions): string | undefined {
|
|
63
|
+
return resolveApiKey(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
|
62
67
|
// options.apiKey may contain sentinel values like "<authenticated>" or "N/A"
|
|
63
68
|
// leaked from the agent loop — only use it if it looks like a real API key.
|
|
64
69
|
const optKey = options?.apiKey;
|
|
65
70
|
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
|
|
66
|
-
return realKey || $
|
|
71
|
+
return realKey || $credentialEnv("GOOGLE_CLOUD_API_KEY");
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function resolveProject(options?: GoogleVertexOptions): string {
|
|
@@ -2,7 +2,7 @@ import * as os from "node:os";
|
|
|
2
2
|
import { scheduler } from "node:timers/promises";
|
|
3
3
|
import {
|
|
4
4
|
$env,
|
|
5
|
-
$
|
|
5
|
+
$pickflag,
|
|
6
6
|
asRecord,
|
|
7
7
|
extractHttpStatusFromError,
|
|
8
8
|
fetchWithRetry,
|
|
@@ -98,7 +98,7 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
98
98
|
serviceTier?: ServiceTier;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
const CODEX_DEBUG = $
|
|
101
|
+
const CODEX_DEBUG = $pickflag("GJC_OPENAI_CODE_DEBUG", "PI_CODEX_DEBUG");
|
|
102
102
|
const CODEX_MAX_RETRIES = 5;
|
|
103
103
|
const CODEX_RETRY_DELAY_MS = 500;
|
|
104
104
|
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
@@ -133,6 +133,31 @@ const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed be
|
|
|
133
133
|
/** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
|
|
134
134
|
const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Tool names the Codex backend reserves for its own namespaces. Sending a
|
|
138
|
+
* function tool under one of these names is rejected with
|
|
139
|
+
* `Function 'computer.computer' not allowed in namespace 'computer'`.
|
|
140
|
+
* These are renamed on the wire and mapped back on receive so the internal
|
|
141
|
+
* tool name stays canonical everywhere else in the harness.
|
|
142
|
+
*/
|
|
143
|
+
const CODEX_RESERVED_TOOL_WIRE_NAMES: ReadonlyMap<string, string> = new Map([
|
|
144
|
+
["browser", "browser_tool"],
|
|
145
|
+
["computer", "computer_tool"],
|
|
146
|
+
]);
|
|
147
|
+
const CODEX_CANONICAL_TOOL_NAMES: ReadonlyMap<string, string> = new Map(
|
|
148
|
+
Array.from(CODEX_RESERVED_TOOL_WIRE_NAMES, ([canonical, wire]) => [wire, canonical]),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/** Maps a canonical tool name to the name Codex accepts on the wire. */
|
|
152
|
+
export function codexToolWireName(name: string): string {
|
|
153
|
+
return CODEX_RESERVED_TOOL_WIRE_NAMES.get(name) ?? name;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Maps a Codex wire tool name back to the canonical harness tool name. */
|
|
157
|
+
export function codexToolCanonicalName(wireName: string): string {
|
|
158
|
+
return CODEX_CANONICAL_TOOL_NAMES.get(wireName) ?? wireName;
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
const CODEX_PROGRESS_EVENT_TYPES = new Set([
|
|
137
162
|
"response.created",
|
|
138
163
|
"response.output_item.added",
|
|
@@ -299,24 +324,34 @@ function parseCodexPositiveInteger(value: string | undefined, fallback: number):
|
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
function isCodexWebSocketEnvEnabled(): boolean {
|
|
302
|
-
return $
|
|
327
|
+
return $pickflag("GJC_OPENAI_CODE_WEBSOCKET", "PI_CODEX_WEBSOCKET");
|
|
303
328
|
}
|
|
304
329
|
|
|
305
330
|
function getCodexWebSocketRetryBudget(options?: Pick<OpenAICodexResponsesOptions, "streamMaxRetries">): number {
|
|
306
331
|
if (options?.streamMaxRetries !== undefined) {
|
|
307
332
|
return resolveRetryBudget(options.streamMaxRetries, CODEX_WEBSOCKET_RETRY_BUDGET);
|
|
308
333
|
}
|
|
309
|
-
return parseCodexNonNegativeInteger(
|
|
334
|
+
return parseCodexNonNegativeInteger(
|
|
335
|
+
$env.GJC_OPENAI_CODE_WEBSOCKET_RETRY_BUDGET ?? $env.PI_CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
336
|
+
CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
337
|
+
);
|
|
310
338
|
}
|
|
311
339
|
|
|
312
340
|
function getCodexWebSocketRetryDelayMs(retry: number): number {
|
|
313
|
-
const baseDelay = parseCodexPositiveInteger(
|
|
341
|
+
const baseDelay = parseCodexPositiveInteger(
|
|
342
|
+
$env.GJC_OPENAI_CODE_WEBSOCKET_RETRY_DELAY_MS ?? $env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS,
|
|
343
|
+
CODEX_RETRY_DELAY_MS,
|
|
344
|
+
);
|
|
314
345
|
return baseDelay * Math.max(1, retry);
|
|
315
346
|
}
|
|
316
347
|
|
|
317
348
|
function getCodexWebSocketIdleTimeoutMs(overrideMs?: number): number {
|
|
318
349
|
return (
|
|
319
|
-
overrideMs ??
|
|
350
|
+
overrideMs ??
|
|
351
|
+
parseCodexPositiveInteger(
|
|
352
|
+
$env.GJC_OPENAI_CODE_WEBSOCKET_IDLE_TIMEOUT_MS ?? $env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS,
|
|
353
|
+
CODEX_WEBSOCKET_IDLE_TIMEOUT_MS,
|
|
354
|
+
)
|
|
320
355
|
);
|
|
321
356
|
}
|
|
322
357
|
|
|
@@ -467,7 +502,7 @@ export function normalizeCodexToolChoice(
|
|
|
467
502
|
: undefined;
|
|
468
503
|
return customTool
|
|
469
504
|
? { type: "custom", name: customTool.customWireName ?? customTool.name }
|
|
470
|
-
: { type: "function", name };
|
|
505
|
+
: { type: "function", name: codexToolWireName(name) };
|
|
471
506
|
};
|
|
472
507
|
if (choice.type === "function") {
|
|
473
508
|
if ("function" in choice && choice.function?.name) {
|
|
@@ -1090,7 +1125,7 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1090
1125
|
return {
|
|
1091
1126
|
type: "toolCall",
|
|
1092
1127
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1093
|
-
name: item.name,
|
|
1128
|
+
name: codexToolCanonicalName(item.name),
|
|
1094
1129
|
arguments: {},
|
|
1095
1130
|
partialJson: item.arguments || "",
|
|
1096
1131
|
};
|
|
@@ -1348,7 +1383,7 @@ function handleOutputItemDone(
|
|
|
1348
1383
|
const toolCall: ToolCall = {
|
|
1349
1384
|
type: "toolCall",
|
|
1350
1385
|
id,
|
|
1351
|
-
name: item.name,
|
|
1386
|
+
name: codexToolCanonicalName(item.name),
|
|
1352
1387
|
arguments: parseStreamingJson(item.arguments || "{}"),
|
|
1353
1388
|
};
|
|
1354
1389
|
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
@@ -2690,6 +2725,13 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2690
2725
|
true,
|
|
2691
2726
|
customCallIds,
|
|
2692
2727
|
);
|
|
2728
|
+
for (const item of outputItems) {
|
|
2729
|
+
// Reconstructed (non-raw) history carries canonical tool names; the
|
|
2730
|
+
// wire form has to match the renamed `tools` entries.
|
|
2731
|
+
if (item.type === "function_call" && typeof item.name === "string") {
|
|
2732
|
+
item.name = codexToolWireName(item.name);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2693
2735
|
if (outputItems.length > 0) {
|
|
2694
2736
|
messages.push(...outputItems);
|
|
2695
2737
|
}
|
|
@@ -2776,7 +2818,7 @@ export function convertOpenAICodexResponsesTools(
|
|
|
2776
2818
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
2777
2819
|
return {
|
|
2778
2820
|
type: "function",
|
|
2779
|
-
name: tool.name,
|
|
2821
|
+
name: codexToolWireName(tool.name),
|
|
2780
2822
|
description: tool.description || "",
|
|
2781
2823
|
parameters,
|
|
2782
2824
|
...(effectiveStrict && { strict: true }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $credentialEnv, $env,
|
|
1
|
+
import { $credentialEnv, $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
ChatCompletionAssistantMessageParam,
|
|
@@ -102,7 +102,9 @@ function resolveOpenAIProviderBaseUrl(
|
|
|
102
102
|
authCredentialType: "api_key" | "oauth" | undefined,
|
|
103
103
|
): string {
|
|
104
104
|
if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
|
|
105
|
-
|
|
105
|
+
// Trusted sources only: this base URL becomes the request endpoint that carries
|
|
106
|
+
// the OpenAI credential, and `$env` merges the caller's `cwd/.env`.
|
|
107
|
+
const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
|
|
106
108
|
const configuredBaseUrl = baseUrl?.trim();
|
|
107
109
|
if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
|
|
108
110
|
return envBaseUrl;
|
|
@@ -110,6 +112,14 @@ function resolveOpenAIProviderBaseUrl(
|
|
|
110
112
|
return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/** Test seam: the provider base URL as resolved from trusted env. */
|
|
116
|
+
export function resolveOpenAICompletionsBaseUrlForTest(
|
|
117
|
+
baseUrl: string | undefined,
|
|
118
|
+
authCredentialType: "api_key" | "oauth" | undefined,
|
|
119
|
+
): string {
|
|
120
|
+
return resolveOpenAIProviderBaseUrl(baseUrl, authCredentialType);
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
/**
|
|
114
124
|
* Normalize tool call ID for Mistral.
|
|
115
125
|
* Mistral requires tool IDs to be exactly 9 alphanumeric characters (a-z, A-Z, 0-9).
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
$credentialEnv,
|
|
3
|
-
$env,
|
|
4
|
-
$inheritedEnv,
|
|
5
|
-
extractHttpStatusFromError,
|
|
6
|
-
logger,
|
|
7
|
-
structuredCloneJSON,
|
|
8
|
-
} from "@gajae-code/utils";
|
|
1
|
+
import { $credentialEnv, extractHttpStatusFromError, logger, structuredCloneJSON } from "@gajae-code/utils";
|
|
9
2
|
import OpenAI from "openai";
|
|
10
3
|
import type {
|
|
11
4
|
Tool as OpenAITool,
|
|
@@ -159,7 +152,10 @@ function resolveOpenAIProviderBaseUrl(
|
|
|
159
152
|
authCredentialType: "api_key" | "oauth" | undefined,
|
|
160
153
|
): string {
|
|
161
154
|
if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
|
|
162
|
-
|
|
155
|
+
// Trusted sources only: this base URL becomes the request endpoint that carries
|
|
156
|
+
// the OpenAI credential, and `$env` merges the caller's `cwd/.env`, so reading it
|
|
157
|
+
// there would let repository content redirect authenticated traffic.
|
|
158
|
+
const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
|
|
163
159
|
const configuredBaseUrl = baseUrl?.trim();
|
|
164
160
|
if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
|
|
165
161
|
return envBaseUrl;
|
|
@@ -167,6 +163,14 @@ function resolveOpenAIProviderBaseUrl(
|
|
|
167
163
|
return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
|
|
168
164
|
}
|
|
169
165
|
|
|
166
|
+
/** Test seam: the provider base URL as resolved from trusted env. */
|
|
167
|
+
export function resolveOpenAIProviderBaseUrlForTest(
|
|
168
|
+
baseUrl: string | undefined,
|
|
169
|
+
authCredentialType: "api_key" | "oauth" | undefined,
|
|
170
|
+
): string {
|
|
171
|
+
return resolveOpenAIProviderBaseUrl(baseUrl, authCredentialType);
|
|
172
|
+
}
|
|
173
|
+
|
|
170
174
|
const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES = new Set([
|
|
171
175
|
"response.created",
|
|
172
176
|
"response.output_item.added",
|
package/src/stream.ts
CHANGED
|
@@ -163,6 +163,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
163
163
|
together: "TOGETHER_API_KEY",
|
|
164
164
|
zenmux: "ZENMUX_API_KEY",
|
|
165
165
|
opengateway: "OPENGATEWAY_API_KEY",
|
|
166
|
+
bizrouter: "BIZROUTER_API_KEY",
|
|
166
167
|
venice: "VENICE_API_KEY",
|
|
167
168
|
vllm: "VLLM_API_KEY",
|
|
168
169
|
xiaomi: "XIAOMI_API_KEY",
|
package/src/types.ts
CHANGED
package/src/usage/claude.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
|
+
import { claudeCodeVersion } from "../providers/anthropic";
|
|
2
3
|
import type {
|
|
3
4
|
CredentialRankingStrategy,
|
|
4
5
|
UsageAmount,
|
|
@@ -31,7 +32,7 @@ const CLAUDE_HEADERS = {
|
|
|
31
32
|
"anthropic-beta":
|
|
32
33
|
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
|
33
34
|
"content-type": "application/json",
|
|
34
|
-
"user-agent":
|
|
35
|
+
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
35
36
|
connection: "keep-alive",
|
|
36
37
|
} as const;
|
|
37
38
|
|
package/src/usage/grok-cli.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $credentialEnv } from "@gajae-code/utils";
|
|
1
2
|
import type {
|
|
2
3
|
CredentialRankingStrategy,
|
|
3
4
|
UsageFetchContext,
|
|
@@ -64,10 +65,20 @@ function isUnsafeGrokBaseUrlOverride(baseUrl?: string): boolean {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function resolveAccessToken(params: UsageFetchParams): string | undefined {
|
|
67
|
-
|
|
68
|
+
// Trusted sources only for the env fallback: this token authenticates the
|
|
69
|
+
// billing/usage call, so whatever can set it decides which account is queried
|
|
70
|
+
// with it. `$env` merges the caller's `cwd/.env` into `process.env`, so
|
|
71
|
+
// reading it there would let repository content supply the credential.
|
|
72
|
+
// Stored credentials keep precedence.
|
|
73
|
+
const token = params.credential.accessToken ?? params.credential.apiKey ?? $credentialEnv("GROK_CLI_OAUTH_TOKEN");
|
|
68
74
|
return token?.trim() || undefined;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/** Test seam: the usage access token as resolved from a credential plus trusted env. */
|
|
78
|
+
export function resolveGrokAccessTokenForTest(params: UsageFetchParams): string | undefined {
|
|
79
|
+
return resolveAccessToken(params);
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
function buildMonthlyUsageLimit(usage: BillingUsage, nowMs: number): UsageLimit {
|
|
72
83
|
const usedFraction = usage.monthlyLimit > 0 ? usage.used / usage.monthlyLimit : 0;
|
|
73
84
|
const percent = usedFraction * 100;
|
package/src/usage/kimi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $credentialEnv } from "@gajae-code/utils";
|
|
2
2
|
import type {
|
|
3
3
|
UsageAmount,
|
|
4
4
|
UsageFetchContext,
|
|
@@ -31,12 +31,26 @@ type KimiUsageRow = {
|
|
|
31
31
|
window?: UsageWindow;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Usage endpoint base, with the environment override resolved from trusted
|
|
36
|
+
* sources only.
|
|
37
|
+
*
|
|
38
|
+
* The result becomes the usage URL that the request sends
|
|
39
|
+
* `Authorization: Bearer <accessToken>` to, so whatever can set it receives the
|
|
40
|
+
* user's Kimi access token. `$env` merges the caller's `cwd/.env`, so reading it
|
|
41
|
+
* there would let repository content collect that token.
|
|
42
|
+
*/
|
|
34
43
|
function normalizeBaseUrl(baseUrl?: string): string {
|
|
35
|
-
const envBase = $
|
|
44
|
+
const envBase = $credentialEnv("KIMI_CODE_BASE_URL");
|
|
36
45
|
const candidate = baseUrl?.trim() || envBase || DEFAULT_BASE_URL;
|
|
37
46
|
return candidate.replace(/\/+$/, "");
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/** Test seam: the usage base URL as resolved from a caller value plus trusted env. */
|
|
50
|
+
export function normalizeKimiUsageBaseUrlForTest(baseUrl?: string): string {
|
|
51
|
+
return normalizeBaseUrl(baseUrl);
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function buildUsageUrl(baseUrl: string): string {
|
|
41
55
|
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
42
56
|
return `${normalized}${USAGE_PATH}`;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `authStorage.getApiKey("anthropic", sessionId)` first, then pass the result
|
|
9
9
|
* through {@link buildAnthropicAuthConfig} for header/URL shaping.
|
|
10
10
|
*/
|
|
11
|
-
import { $
|
|
11
|
+
import { $credentialEnv } from "@gajae-code/utils";
|
|
12
12
|
import {
|
|
13
13
|
buildAnthropicHeaders as buildProviderAnthropicHeaders,
|
|
14
14
|
normalizeAnthropicBaseUrl,
|
|
@@ -29,12 +29,20 @@ function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
|
29
29
|
return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the Anthropic base URL from the environment.
|
|
34
|
+
*
|
|
35
|
+
* Trusted sources only: the result becomes the request URL that carries the
|
|
36
|
+
* Anthropic API key / OAuth token, so whatever can set it can redirect
|
|
37
|
+
* authenticated traffic. `$env` merges the caller's `cwd/.env`, so reading it
|
|
38
|
+
* there would let repository content choose where credentials are sent.
|
|
39
|
+
*/
|
|
32
40
|
export function resolveAnthropicBaseUrlFromEnv(): string | undefined {
|
|
33
41
|
if (isFoundryEnabled()) {
|
|
34
|
-
const foundryBaseUrl = normalizeBaseUrl($
|
|
42
|
+
const foundryBaseUrl = normalizeBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
|
|
35
43
|
if (foundryBaseUrl) return foundryBaseUrl;
|
|
36
44
|
}
|
|
37
|
-
const anthropicBaseUrl = normalizeBaseUrl($
|
|
45
|
+
const anthropicBaseUrl = normalizeBaseUrl($credentialEnv("ANTHROPIC_BASE_URL"));
|
|
38
46
|
return anthropicBaseUrl || undefined;
|
|
39
47
|
}
|
|
40
48
|
|
package/src/utils/foundry.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $credentialEnv } from "@gajae-code/utils";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Whether Anthropic requests run in Foundry gateway mode.
|
|
5
|
+
*
|
|
6
|
+
* Resolved from trusted environment sources only. Enabling Foundry switches the
|
|
7
|
+
* request base URL and injects TLS client material, so whatever can set this
|
|
8
|
+
* redirects authenticated traffic. `$env` merges the caller's `cwd/.env`, so
|
|
9
|
+
* reading it there would let repository content flip the mode; resolve it the
|
|
10
|
+
* same way the credentials themselves are (launching shell plus GJC/user-owned
|
|
11
|
+
* `.env` files, never the project `.env`).
|
|
12
|
+
*/
|
|
3
13
|
export function isFoundryEnabled(): boolean {
|
|
4
|
-
const value = $
|
|
14
|
+
const value = $credentialEnv("CLAUDE_CODE_USE_FOUNDRY");
|
|
5
15
|
if (!value) return false;
|
|
6
16
|
const normalized = value.trim().toLowerCase();
|
|
7
17
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
@@ -26,6 +26,34 @@ type ErrorWithStatus = {
|
|
|
26
26
|
|
|
27
27
|
const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Connection-level failure codes, meaning the request never reached the
|
|
31
|
+
* provider and no HTTP status exists. Bun reports the first group for `fetch`;
|
|
32
|
+
* the `E*`/`UND_ERR_*` group comes from Node-style DNS and socket errors.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately excludes aborts and TLS/certificate codes: an abort is a
|
|
35
|
+
* user/watchdog outcome with its own display path, and its message must keep
|
|
36
|
+
* matching the abort normalizers in `modes/utils/abort-message`.
|
|
37
|
+
*/
|
|
38
|
+
const TRANSPORT_FAILURE_CODES: ReadonlySet<string> = new Set([
|
|
39
|
+
"ConnectionClosed",
|
|
40
|
+
"ConnectionRefused",
|
|
41
|
+
"ConnectionReset",
|
|
42
|
+
"ConnectionTimeout",
|
|
43
|
+
"FailedToOpenSocket",
|
|
44
|
+
"HTTP2Unsupported",
|
|
45
|
+
"EAI_AGAIN",
|
|
46
|
+
"ECONNREFUSED",
|
|
47
|
+
"ECONNRESET",
|
|
48
|
+
"EHOSTUNREACH",
|
|
49
|
+
"ENETUNREACH",
|
|
50
|
+
"ENOTFOUND",
|
|
51
|
+
"EPIPE",
|
|
52
|
+
"ETIMEDOUT",
|
|
53
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
54
|
+
"UND_ERR_SOCKET",
|
|
55
|
+
]);
|
|
56
|
+
|
|
29
57
|
/**
|
|
30
58
|
* Privacy note appended next to a saved raw HTTP request dump. The dump is
|
|
31
59
|
* sanitized (secrets/thinking redacted) but can still contain prompt content
|
|
@@ -88,6 +116,54 @@ export async function appendRawHttpRequestDumpFor400(
|
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
|
|
119
|
+
/** Origin and path of `value`, dropping query, fragment, and credentials so a
|
|
120
|
+
* key carried in the request URL (Google `?key=`, signed URLs) never lands in
|
|
121
|
+
* a user-visible error string. */
|
|
122
|
+
function redactRequestUrl(value: unknown): string | undefined {
|
|
123
|
+
if (typeof value !== "string" || value.trim().length === 0) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(value);
|
|
126
|
+
return `${url.origin}${url.pathname}`;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findTransportFailure(error: unknown, depth: number): { code: string; url?: string } | undefined {
|
|
133
|
+
if (!error || typeof error !== "object" || depth > 2) return undefined;
|
|
134
|
+
const info = error as { code?: unknown; path?: unknown; url?: unknown; cause?: unknown };
|
|
135
|
+
if (typeof info.code === "string" && TRANSPORT_FAILURE_CODES.has(info.code)) {
|
|
136
|
+
return { code: info.code, url: redactRequestUrl(info.path) ?? redactRequestUrl(info.url) };
|
|
137
|
+
}
|
|
138
|
+
return findTransportFailure(info.cause, depth + 1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Name the failed connection when the request never produced an HTTP status.
|
|
143
|
+
*
|
|
144
|
+
* Bun raises DNS and socket failures as a bare `Error` whose message is a
|
|
145
|
+
* standalone hint ("Was there a typo in the url or port?", "Unable to connect.
|
|
146
|
+
* Is the computer able to access the url?") while the actionable facts live on
|
|
147
|
+
* `code` and `path`. Those properties are dropped when only `message` reaches
|
|
148
|
+
* the assistant message, so a provider outage, a local DNS failure, and a
|
|
149
|
+
* mistyped custom base URL all render as the same context-free sentence.
|
|
150
|
+
* Appending the code and the target URL tells the user which host failed and
|
|
151
|
+
* whether the fault is theirs.
|
|
152
|
+
*/
|
|
153
|
+
export function appendTransportFailureContext(
|
|
154
|
+
message: string,
|
|
155
|
+
error: unknown,
|
|
156
|
+
rawRequestDump: RawHttpRequestDump | undefined,
|
|
157
|
+
): string {
|
|
158
|
+
if (extractHttpStatusFromError(error) !== undefined) return message;
|
|
159
|
+
const failure = findTransportFailure(error, 0);
|
|
160
|
+
if (!failure) return message;
|
|
161
|
+
|
|
162
|
+
const url = failure.url ?? redactRequestUrl(rawRequestDump?.url);
|
|
163
|
+
const context = url ? `transport=${failure.code} url=${url}` : `transport=${failure.code}`;
|
|
164
|
+
return message.includes(context) ? message : `${message} (${context})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
91
167
|
export async function finalizeErrorMessage(
|
|
92
168
|
error: unknown,
|
|
93
169
|
rawRequestDump: RawHttpRequestDump | undefined,
|
|
@@ -105,6 +181,7 @@ export async function finalizeErrorMessage(
|
|
|
105
181
|
if (isModelUnavailableError(message, error)) {
|
|
106
182
|
message = `${message}\n\n${formatModelUnavailableGuidance(rawRequestDump)}`;
|
|
107
183
|
}
|
|
184
|
+
message = appendTransportFailureContext(message, error, rawRequestDump);
|
|
108
185
|
return appendRawHttpRequestDumpFor400(message, error, rawRequestDump);
|
|
109
186
|
}
|
|
110
187
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** BizRouter login flow (API key paste, validated via /v1/models). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginBizRouter = createApiKeyLogin({
|
|
5
|
+
providerLabel: "BizRouter",
|
|
6
|
+
authUrl: "https://bizrouter.ai/settings/keys",
|
|
7
|
+
instructions: "Create or copy your BizRouter API key",
|
|
8
|
+
promptMessage: "Paste your BizRouter API key",
|
|
9
|
+
placeholder: "sk-br-v1-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "models-endpoint",
|
|
12
|
+
provider: "BizRouter",
|
|
13
|
+
modelsUrl: "https://api.bizrouter.ai/v1/models",
|
|
14
|
+
},
|
|
15
|
+
});
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -240,6 +240,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
240
240
|
name: "ZenMux",
|
|
241
241
|
available: true,
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
id: "bizrouter",
|
|
245
|
+
name: "BizRouter",
|
|
246
|
+
available: true,
|
|
247
|
+
},
|
|
243
248
|
{
|
|
244
249
|
id: "opengateway",
|
|
245
250
|
name: "OpenGateway by Sionic AI",
|
|
@@ -391,6 +396,7 @@ export async function refreshOAuthToken(
|
|
|
391
396
|
case "vercel-ai-gateway":
|
|
392
397
|
case "qwen-portal":
|
|
393
398
|
case "zenmux":
|
|
399
|
+
case "bizrouter":
|
|
394
400
|
case "opengateway":
|
|
395
401
|
case "vllm":
|
|
396
402
|
// API keys / static bearer tokens don't expire, return as-is
|