@gajae-code/ai 0.18.0 → 0.18.5
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 +41 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +5 -2
- package/dist/types/providers/anthropic.d.ts +0 -1
- package/dist/types/providers/claude-code-version.d.ts +39 -0
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/model-pricing.ts +10 -0
- package/src/models.json +210 -0
- package/src/provider-models/openai-compat.ts +40 -3
- package/src/providers/anthropic.d.ts +0 -1
- package/src/providers/anthropic.ts +28 -4
- package/src/providers/claude-code-version.d.ts +39 -0
- package/src/providers/claude-code-version.ts +205 -0
- package/src/providers/kiro-codewhisperer.ts +55 -25
- package/src/providers/openai-codex-responses.ts +23 -1
- package/src/providers/openai-completions.ts +19 -1
- package/src/usage/claude.ts +2 -2
- package/dist/types/utils/h2-fetch.d.ts +0 -35
- package/src/utils/h2-fetch.ts +0 -133
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.5] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
## [0.18.4] - 2026-09-30
|
|
8
|
+
|
|
9
|
+
## [0.18.3] - 2026-09-30
|
|
10
|
+
|
|
11
|
+
## [0.18.2] - 2026-09-30
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Bundled Claude Sonnet 5.5: `anthropic/claude-sonnet-5-5` and Amazon Bedrock `anthropic.claude-sonnet-5-5` (plus `au.`/`eu.`/`global.`/`jp.`/`us.` inference profiles), with 1M context, 128K output, and $2/$10 per MTok pricing (#6111).
|
|
16
|
+
|
|
17
|
+
- Bundled GPT-6.1 Sol for the Codex provider: `openai-codex/gpt-6.1-sol` (272K context, 128K output, reasoning effort low..max) with $2/$10 per MTok and $0.10 cached-input pricing (#6147). Presets and model-profile defaults are unchanged.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Removed `installH2Fetch`. Bun's HTTP/2 client can wedge a pooled connection while uploading a large request body (~300 KB, a typical long agent context): every later request on that connection waits indefinitely for response headers. All `fetch()` traffic now uses HTTP/1.1.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A large (≥1 MB) request to a custom Anthropic-compatible endpoint is now retried when its connection drops before any response (ECONNRESET, socket closed, `Connection error`). Previously the one-upload ceiling meant for first-event stalls also covered these failures, so a single network blip ended the turn. Server responses (e.g. 529) and first-event timeouts still respect the ceiling (#6072).
|
|
26
|
+
|
|
27
|
+
- Kiro (OAuth CodeWhisperer transport) now replays earlier tool calls as structured `toolUses` and sends each tool result as `{ toolUseId, status, content: [{ text }] }`, linked to the call it answers, instead of serializing calls into assistant text and nesting results. Parallel tool results share one history entry, and image results are marked `[image omitted]` rather than sent empty (#6079).
|
|
28
|
+
|
|
29
|
+
- OpenAI-compatible completions requests now log OpenAI SDK connection timeouts and retries to the gjc log. Previously a request stalled before response headers retried silently for up to several minutes.
|
|
30
|
+
|
|
31
|
+
## [0.18.1] - 2026-09-29
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Bump the bundled Claude Code fingerprint floor from 2.1.281 to 2.1.284.
|
|
36
|
+
- Anthropic OAuth requests now sign `claude-cli/<version>` and the billing `cc_version` with the latest published Claude Code release instead of a build-time constant. The version is resolved in the background from Anthropic's `latest` release channel and the npm dist-tag (higher wins), refreshed at most every 6 hours, cached in `~/.gjc/cache/claude-code-version.json`, and never blocks a request or drops below the bundled floor. Set `GJC_CLAUDE_CODE_VERSION=X.Y.Z` to pin it. A stale fingerprint is answered with HTTP 400 `claude_code_version_too_old` on newer models, so this no longer waits for a gjc release to recover.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- OpenGateway models discovered from `/v1/models` now take their capabilities from the bundled catalog entry for the same upstream model, and `-ultrafast` deployments resolve to their upstream model, so `opengateway/deepseek/deepseek-v4.1-flash-ultrafast` and the GLM/Kimi ultrafast models are registered with reasoning support and accept thinking levels (#6070).
|
|
41
|
+
|
|
42
|
+
- Bound Codex turns that stall after an empty reasoning item and streamed tool-call start to the shared stream idle timeout.
|
|
43
|
+
|
|
44
|
+
- Apply the shared stream idle timeout to Codex WebSocket responses when no WebSocket-specific timeout is configured, so a stalled preferred transport surfaces an error within the expected idle window.
|
|
45
|
+
|
|
5
46
|
## [0.18.0] - 2026-09-27
|
|
6
47
|
|
|
7
48
|
### Added
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./provider-diagnostic";
|
|
|
14
14
|
export * from "./provider-models";
|
|
15
15
|
export * from "./providers/anthropic";
|
|
16
16
|
export * from "./providers/azure-openai-responses";
|
|
17
|
+
export * from "./providers/claude-code-version";
|
|
17
18
|
export type * from "./providers/cursor";
|
|
18
19
|
export * from "./providers/gitlab-duo";
|
|
19
20
|
export type * from "./providers/google";
|
|
@@ -45,7 +46,6 @@ export * from "./utils/anthropic-auth";
|
|
|
45
46
|
export * from "./utils/discovery";
|
|
46
47
|
export * from "./utils/event-stream";
|
|
47
48
|
export * from "./utils/fallback-transport";
|
|
48
|
-
export * from "./utils/h2-fetch";
|
|
49
49
|
export * from "./utils/oauth";
|
|
50
50
|
export type { OAuthCredentials, OAuthLoginOptions, OAuthProvider, OAuthProviderId, OAuthProviderInfo, } from "./utils/oauth/types";
|
|
51
51
|
export * from "./utils/overflow";
|
|
@@ -121,8 +121,11 @@ export interface OpenGatewayModelManagerConfig {
|
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* OpenGateway by Sionic AI — an OpenAI-compatible gateway that fronts OpenAI,
|
|
124
|
-
* Anthropic, and
|
|
125
|
-
* the OpenAI-compatible `/v1/models` endpoint
|
|
124
|
+
* Anthropic, Google, DeepSeek, Z.ai, Moonshot and Qwen models behind one API key.
|
|
125
|
+
* Models are discovered from the OpenAI-compatible `/v1/models` endpoint, which
|
|
126
|
+
* reports no capability metadata, so capabilities come from the bundled catalog:
|
|
127
|
+
* the OpenGateway entry first, then the same upstream id from any bundled
|
|
128
|
+
* provider, with `-ultrafast` deployments resolved to their upstream model.
|
|
126
129
|
*/
|
|
127
130
|
export declare function opengatewayModelManagerOptions(config?: OpenGatewayModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
128
131
|
export interface BizRouterModelManagerConfig {
|
|
@@ -107,7 +107,6 @@ export interface CpaToolAliasRestoreFailure {
|
|
|
107
107
|
*/
|
|
108
108
|
export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
|
|
109
109
|
export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
|
|
110
|
-
export declare const claudeCodeVersion = "2.1.281";
|
|
111
110
|
export declare const claudeCodeEntrypoint = "sdk-cli";
|
|
112
111
|
export declare const claudeToolPrefix: string;
|
|
113
112
|
export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Bundled floor; `scripts/check-spoofed-versions.ts --update` keeps it current. */
|
|
2
|
+
export declare const CLAUDE_CODE_BASELINE_VERSION = "2.1.284";
|
|
3
|
+
/** Pins the fingerprint and disables network resolution. */
|
|
4
|
+
export declare const CLAUDE_CODE_VERSION_ENV = "GJC_CLAUDE_CODE_VERSION";
|
|
5
|
+
/**
|
|
6
|
+
* Anthropic's native-installer release channel. `stable` deliberately lags the
|
|
7
|
+
* npm package during a staged rollout, so `latest` is the channel that matches
|
|
8
|
+
* what a freshly updated Claude Code actually reports.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLAUDE_RELEASE_CHANNEL_URL = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest";
|
|
11
|
+
export declare const CLAUDE_NPM_DIST_TAG_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
12
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
export declare function compareClaudeCodeVersions(a: string, b: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Latest published Claude Code version: the higher of the native-installer
|
|
16
|
+
* `latest` channel and the npm dist-tag, so one lagging or unavailable source
|
|
17
|
+
* cannot pin the fingerprint back. `null` when both sources fail.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchLatestClaudeCodeVersion(fetchImpl?: FetchLike, userAgent?: string): Promise<string | null>;
|
|
20
|
+
export interface ClaudeCodeVersionResolverOptions {
|
|
21
|
+
baseline: string;
|
|
22
|
+
/** Exact pin; disables network resolution when set. */
|
|
23
|
+
pinned?: string;
|
|
24
|
+
/** `null` disables both disk caching and network resolution. */
|
|
25
|
+
cachePath: string | null;
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
refreshIntervalMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ClaudeCodeVersionResolver {
|
|
31
|
+
/** Current fingerprint; schedules a background refresh when the cache is stale. */
|
|
32
|
+
get(): string;
|
|
33
|
+
/** Refresh now (deduplicated); resolves with the fingerprint afterwards. */
|
|
34
|
+
refresh(): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
export declare function createClaudeCodeVersionResolver(options: ClaudeCodeVersionResolverOptions): ClaudeCodeVersionResolver;
|
|
37
|
+
/** Claude Code version the `claude-cli` fingerprint and billing attribution sign with. */
|
|
38
|
+
export declare function getClaudeCodeVersion(): string;
|
|
39
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.5",
|
|
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",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
42
42
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
43
43
|
"@bufbuild/protobuf": "^2.12.0",
|
|
44
|
-
"@gajae-code/natives": "0.18.
|
|
45
|
-
"@gajae-code/utils": "0.18.
|
|
44
|
+
"@gajae-code/natives": "0.18.5",
|
|
45
|
+
"@gajae-code/utils": "0.18.5",
|
|
46
46
|
"openai": "^6.36.0",
|
|
47
47
|
"partial-json": "^0.1.7",
|
|
48
48
|
"zod": "4.4.3"
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./provider-diagnostic";
|
|
|
14
14
|
export * from "./provider-models";
|
|
15
15
|
export * from "./providers/anthropic";
|
|
16
16
|
export * from "./providers/azure-openai-responses";
|
|
17
|
+
export * from "./providers/claude-code-version";
|
|
17
18
|
export type * from "./providers/cursor";
|
|
18
19
|
export * from "./providers/gitlab-duo";
|
|
19
20
|
export type * from "./providers/google";
|
|
@@ -45,7 +46,6 @@ export * from "./utils/anthropic-auth";
|
|
|
45
46
|
export * from "./utils/discovery";
|
|
46
47
|
export * from "./utils/event-stream";
|
|
47
48
|
export * from "./utils/fallback-transport";
|
|
48
|
-
export * from "./utils/h2-fetch";
|
|
49
49
|
export * from "./utils/oauth";
|
|
50
50
|
export type {
|
|
51
51
|
OAuthCredentials,
|
package/src/model-pricing.ts
CHANGED
|
@@ -35,6 +35,15 @@ const GPT_6_SOL_PRICING: TieredPricing = {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
// TODO(gpt-6.1-sol): cache-write and long-context prices not yet published; derived from gpt-6-sol / 2x-1.5x rule
|
|
39
|
+
const GPT_6_1_SOL_PRICING: TieredPricing = {
|
|
40
|
+
cost: { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 },
|
|
41
|
+
longContextPricing: {
|
|
42
|
+
threshold: LONG_CONTEXT_THRESHOLD,
|
|
43
|
+
cost: { input: 4, output: 15, cacheRead: 0.2, cacheWrite: 5 },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
38
47
|
// GPT-6 Luna: $0.10/$0.50 standard, cache read $0.01, cache write $0.125;
|
|
39
48
|
// inputs past 272K apply 2x to input/cache and 1.5x to output.
|
|
40
49
|
const GPT_6_LUNA_PRICING: TieredPricing = {
|
|
@@ -49,6 +58,7 @@ const GPT_6_LUNA_PRICING: TieredPricing = {
|
|
|
49
58
|
const OPENAI_GPT_5_6_PRICING: ReadonlyMap<string, TieredPricing> = new Map([
|
|
50
59
|
["gpt-6-astra", GPT_6_ASTRA_PRICING],
|
|
51
60
|
["gpt-6-sol", GPT_6_SOL_PRICING],
|
|
61
|
+
["gpt-6.1-sol", GPT_6_1_SOL_PRICING],
|
|
52
62
|
["gpt-6-luna", GPT_6_LUNA_PRICING],
|
|
53
63
|
["gpt-5.6", GPT_5_6_SOL_PRICING],
|
|
54
64
|
["gpt-5.6-sol", GPT_5_6_SOL_PRICING],
|
package/src/models.json
CHANGED
|
@@ -964,6 +964,31 @@
|
|
|
964
964
|
"maxLevel": "high"
|
|
965
965
|
}
|
|
966
966
|
},
|
|
967
|
+
"anthropic.claude-sonnet-5-5": {
|
|
968
|
+
"id": "anthropic.claude-sonnet-5-5",
|
|
969
|
+
"name": "Anthropic Sonnet 5.5",
|
|
970
|
+
"api": "bedrock-converse-stream",
|
|
971
|
+
"provider": "amazon-bedrock",
|
|
972
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
973
|
+
"reasoning": true,
|
|
974
|
+
"input": [
|
|
975
|
+
"text",
|
|
976
|
+
"image"
|
|
977
|
+
],
|
|
978
|
+
"cost": {
|
|
979
|
+
"input": 2,
|
|
980
|
+
"output": 10,
|
|
981
|
+
"cacheRead": 0.2,
|
|
982
|
+
"cacheWrite": 2.5
|
|
983
|
+
},
|
|
984
|
+
"contextWindow": 1000000,
|
|
985
|
+
"maxTokens": 128000,
|
|
986
|
+
"thinking": {
|
|
987
|
+
"mode": "anthropic-budget-effort",
|
|
988
|
+
"minLevel": "minimal",
|
|
989
|
+
"maxLevel": "high"
|
|
990
|
+
}
|
|
991
|
+
},
|
|
967
992
|
"apac.amazon.nova-lite-v1:0": {
|
|
968
993
|
"id": "apac.amazon.nova-lite-v1:0",
|
|
969
994
|
"name": "Nova Lite (APAC)",
|
|
@@ -1308,6 +1333,31 @@
|
|
|
1308
1333
|
"maxLevel": "high"
|
|
1309
1334
|
}
|
|
1310
1335
|
},
|
|
1336
|
+
"au.anthropic.claude-sonnet-5-5": {
|
|
1337
|
+
"id": "au.anthropic.claude-sonnet-5-5",
|
|
1338
|
+
"name": "Anthropic Sonnet 5.5 (AU)",
|
|
1339
|
+
"api": "bedrock-converse-stream",
|
|
1340
|
+
"provider": "amazon-bedrock",
|
|
1341
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
1342
|
+
"reasoning": true,
|
|
1343
|
+
"input": [
|
|
1344
|
+
"text",
|
|
1345
|
+
"image"
|
|
1346
|
+
],
|
|
1347
|
+
"cost": {
|
|
1348
|
+
"input": 2.2,
|
|
1349
|
+
"output": 11,
|
|
1350
|
+
"cacheRead": 0.22,
|
|
1351
|
+
"cacheWrite": 2.75
|
|
1352
|
+
},
|
|
1353
|
+
"contextWindow": 1000000,
|
|
1354
|
+
"maxTokens": 128000,
|
|
1355
|
+
"thinking": {
|
|
1356
|
+
"mode": "anthropic-budget-effort",
|
|
1357
|
+
"minLevel": "minimal",
|
|
1358
|
+
"maxLevel": "high"
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1311
1361
|
"ca.amazon.nova-lite-v1:0": {
|
|
1312
1362
|
"id": "ca.amazon.nova-lite-v1:0",
|
|
1313
1363
|
"name": "Nova Lite (CA)",
|
|
@@ -2072,6 +2122,31 @@
|
|
|
2072
2122
|
"maxLevel": "high"
|
|
2073
2123
|
}
|
|
2074
2124
|
},
|
|
2125
|
+
"eu.anthropic.claude-sonnet-5-5": {
|
|
2126
|
+
"id": "eu.anthropic.claude-sonnet-5-5",
|
|
2127
|
+
"name": "Anthropic Sonnet 5.5 (EU)",
|
|
2128
|
+
"api": "bedrock-converse-stream",
|
|
2129
|
+
"provider": "amazon-bedrock",
|
|
2130
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
2131
|
+
"reasoning": true,
|
|
2132
|
+
"input": [
|
|
2133
|
+
"text",
|
|
2134
|
+
"image"
|
|
2135
|
+
],
|
|
2136
|
+
"cost": {
|
|
2137
|
+
"input": 2.2,
|
|
2138
|
+
"output": 11,
|
|
2139
|
+
"cacheRead": 0.22,
|
|
2140
|
+
"cacheWrite": 2.75
|
|
2141
|
+
},
|
|
2142
|
+
"contextWindow": 1000000,
|
|
2143
|
+
"maxTokens": 128000,
|
|
2144
|
+
"thinking": {
|
|
2145
|
+
"mode": "anthropic-budget-effort",
|
|
2146
|
+
"minLevel": "minimal",
|
|
2147
|
+
"maxLevel": "high"
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2075
2150
|
"eu.mistral.pixtral-large-2502-v1:0": {
|
|
2076
2151
|
"id": "eu.mistral.pixtral-large-2502-v1:0",
|
|
2077
2152
|
"name": "Pixtral Large (25.02) (EU)",
|
|
@@ -2477,6 +2552,31 @@
|
|
|
2477
2552
|
"maxLevel": "high"
|
|
2478
2553
|
}
|
|
2479
2554
|
},
|
|
2555
|
+
"global.anthropic.claude-sonnet-5-5": {
|
|
2556
|
+
"id": "global.anthropic.claude-sonnet-5-5",
|
|
2557
|
+
"name": "Anthropic Sonnet 5.5 (Global)",
|
|
2558
|
+
"api": "bedrock-converse-stream",
|
|
2559
|
+
"provider": "amazon-bedrock",
|
|
2560
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
2561
|
+
"reasoning": true,
|
|
2562
|
+
"input": [
|
|
2563
|
+
"text",
|
|
2564
|
+
"image"
|
|
2565
|
+
],
|
|
2566
|
+
"cost": {
|
|
2567
|
+
"input": 2,
|
|
2568
|
+
"output": 10,
|
|
2569
|
+
"cacheRead": 0.2,
|
|
2570
|
+
"cacheWrite": 2.5
|
|
2571
|
+
},
|
|
2572
|
+
"contextWindow": 1000000,
|
|
2573
|
+
"maxTokens": 128000,
|
|
2574
|
+
"thinking": {
|
|
2575
|
+
"mode": "anthropic-budget-effort",
|
|
2576
|
+
"minLevel": "minimal",
|
|
2577
|
+
"maxLevel": "high"
|
|
2578
|
+
}
|
|
2579
|
+
},
|
|
2480
2580
|
"global.openai.gpt-5.6-luna": {
|
|
2481
2581
|
"id": "global.openai.gpt-5.6-luna",
|
|
2482
2582
|
"name": "GPT-5.6 Luna (Global)",
|
|
@@ -3020,6 +3120,31 @@
|
|
|
3020
3120
|
"maxLevel": "high"
|
|
3021
3121
|
}
|
|
3022
3122
|
},
|
|
3123
|
+
"jp.anthropic.claude-sonnet-5-5": {
|
|
3124
|
+
"id": "jp.anthropic.claude-sonnet-5-5",
|
|
3125
|
+
"name": "Anthropic Sonnet 5.5 (JP)",
|
|
3126
|
+
"api": "bedrock-converse-stream",
|
|
3127
|
+
"provider": "amazon-bedrock",
|
|
3128
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
3129
|
+
"reasoning": true,
|
|
3130
|
+
"input": [
|
|
3131
|
+
"text",
|
|
3132
|
+
"image"
|
|
3133
|
+
],
|
|
3134
|
+
"cost": {
|
|
3135
|
+
"input": 2.2,
|
|
3136
|
+
"output": 11,
|
|
3137
|
+
"cacheRead": 0.22,
|
|
3138
|
+
"cacheWrite": 2.75
|
|
3139
|
+
},
|
|
3140
|
+
"contextWindow": 1000000,
|
|
3141
|
+
"maxTokens": 128000,
|
|
3142
|
+
"thinking": {
|
|
3143
|
+
"mode": "anthropic-budget-effort",
|
|
3144
|
+
"minLevel": "minimal",
|
|
3145
|
+
"maxLevel": "high"
|
|
3146
|
+
}
|
|
3147
|
+
},
|
|
3023
3148
|
"meta.llama3-1-405b-instruct-v1:0": {
|
|
3024
3149
|
"id": "meta.llama3-1-405b-instruct-v1:0",
|
|
3025
3150
|
"name": "Llama 3.1 405B Instruct",
|
|
@@ -4501,6 +4626,31 @@
|
|
|
4501
4626
|
"maxLevel": "high"
|
|
4502
4627
|
}
|
|
4503
4628
|
},
|
|
4629
|
+
"us.anthropic.claude-sonnet-5-5": {
|
|
4630
|
+
"id": "us.anthropic.claude-sonnet-5-5",
|
|
4631
|
+
"name": "Anthropic Sonnet 5.5 (US)",
|
|
4632
|
+
"api": "bedrock-converse-stream",
|
|
4633
|
+
"provider": "amazon-bedrock",
|
|
4634
|
+
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
4635
|
+
"reasoning": true,
|
|
4636
|
+
"input": [
|
|
4637
|
+
"text",
|
|
4638
|
+
"image"
|
|
4639
|
+
],
|
|
4640
|
+
"cost": {
|
|
4641
|
+
"input": 2.2,
|
|
4642
|
+
"output": 11,
|
|
4643
|
+
"cacheRead": 0.22,
|
|
4644
|
+
"cacheWrite": 2.75
|
|
4645
|
+
},
|
|
4646
|
+
"contextWindow": 1000000,
|
|
4647
|
+
"maxTokens": 128000,
|
|
4648
|
+
"thinking": {
|
|
4649
|
+
"mode": "anthropic-budget-effort",
|
|
4650
|
+
"minLevel": "minimal",
|
|
4651
|
+
"maxLevel": "high"
|
|
4652
|
+
}
|
|
4653
|
+
},
|
|
4504
4654
|
"us.deepseek.r1-v1:0": {
|
|
4505
4655
|
"id": "us.deepseek.r1-v1:0",
|
|
4506
4656
|
"name": "DeepSeek-R1 (US)",
|
|
@@ -5656,6 +5806,31 @@
|
|
|
5656
5806
|
"minLevel": "minimal",
|
|
5657
5807
|
"maxLevel": "max"
|
|
5658
5808
|
}
|
|
5809
|
+
},
|
|
5810
|
+
"claude-sonnet-5-5": {
|
|
5811
|
+
"id": "claude-sonnet-5-5",
|
|
5812
|
+
"name": "Anthropic Sonnet 5.5",
|
|
5813
|
+
"api": "anthropic-messages",
|
|
5814
|
+
"provider": "anthropic",
|
|
5815
|
+
"baseUrl": "https://api.anthropic.com",
|
|
5816
|
+
"reasoning": true,
|
|
5817
|
+
"input": [
|
|
5818
|
+
"text",
|
|
5819
|
+
"image"
|
|
5820
|
+
],
|
|
5821
|
+
"cost": {
|
|
5822
|
+
"input": 2,
|
|
5823
|
+
"output": 10,
|
|
5824
|
+
"cacheRead": 0.2,
|
|
5825
|
+
"cacheWrite": 2.5
|
|
5826
|
+
},
|
|
5827
|
+
"contextWindow": 1000000,
|
|
5828
|
+
"maxTokens": 128000,
|
|
5829
|
+
"thinking": {
|
|
5830
|
+
"mode": "anthropic-adaptive",
|
|
5831
|
+
"minLevel": "minimal",
|
|
5832
|
+
"maxLevel": "max"
|
|
5833
|
+
}
|
|
5659
5834
|
}
|
|
5660
5835
|
},
|
|
5661
5836
|
"azure-openai": {
|
|
@@ -68026,6 +68201,41 @@
|
|
|
68026
68201
|
}
|
|
68027
68202
|
}
|
|
68028
68203
|
},
|
|
68204
|
+
"gpt-6.1-sol": {
|
|
68205
|
+
"id": "gpt-6.1-sol",
|
|
68206
|
+
"name": "GPT-6.1-Sol",
|
|
68207
|
+
"api": "openai-codex-responses",
|
|
68208
|
+
"provider": "openai-codex",
|
|
68209
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
68210
|
+
"reasoning": true,
|
|
68211
|
+
"input": [
|
|
68212
|
+
"text",
|
|
68213
|
+
"image"
|
|
68214
|
+
],
|
|
68215
|
+
"cost": {
|
|
68216
|
+
"input": 2,
|
|
68217
|
+
"output": 10,
|
|
68218
|
+
"cacheRead": 0.1,
|
|
68219
|
+
"cacheWrite": 2.5
|
|
68220
|
+
},
|
|
68221
|
+
"contextWindow": 272000,
|
|
68222
|
+
"maxTokens": 128000,
|
|
68223
|
+
"preferWebsockets": true,
|
|
68224
|
+
"thinking": {
|
|
68225
|
+
"mode": "effort",
|
|
68226
|
+
"minLevel": "low",
|
|
68227
|
+
"maxLevel": "max"
|
|
68228
|
+
},
|
|
68229
|
+
"longContextPricing": {
|
|
68230
|
+
"threshold": 272000,
|
|
68231
|
+
"cost": {
|
|
68232
|
+
"input": 4,
|
|
68233
|
+
"output": 15,
|
|
68234
|
+
"cacheRead": 0.2,
|
|
68235
|
+
"cacheWrite": 5
|
|
68236
|
+
}
|
|
68237
|
+
}
|
|
68238
|
+
},
|
|
68029
68239
|
"gpt-daybreak-blue-latest": {
|
|
68030
68240
|
"id": "gpt-daybreak-blue-latest",
|
|
68031
68241
|
"name": "Daybreak Blue",
|
|
@@ -1175,15 +1175,52 @@ export interface OpenGatewayModelManagerConfig {
|
|
|
1175
1175
|
baseUrl?: string;
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
|
+
const OPENGATEWAY_BASE_URL = "https://apis.opengateway.ai/v1";
|
|
1179
|
+
/** OpenGateway serves accelerated deployments of an upstream model as `<upstream id>-ultrafast`. */
|
|
1180
|
+
const OPENGATEWAY_ULTRAFAST_SUFFIX = "-ultrafast";
|
|
1181
|
+
|
|
1178
1182
|
/**
|
|
1179
1183
|
* OpenGateway by Sionic AI — an OpenAI-compatible gateway that fronts OpenAI,
|
|
1180
|
-
* Anthropic, and
|
|
1181
|
-
* the OpenAI-compatible `/v1/models` endpoint
|
|
1184
|
+
* Anthropic, Google, DeepSeek, Z.ai, Moonshot and Qwen models behind one API key.
|
|
1185
|
+
* Models are discovered from the OpenAI-compatible `/v1/models` endpoint, which
|
|
1186
|
+
* reports no capability metadata, so capabilities come from the bundled catalog:
|
|
1187
|
+
* the OpenGateway entry first, then the same upstream id from any bundled
|
|
1188
|
+
* provider, with `-ultrafast` deployments resolved to their upstream model.
|
|
1182
1189
|
*/
|
|
1183
1190
|
export function opengatewayModelManagerOptions(
|
|
1184
1191
|
config?: OpenGatewayModelManagerConfig,
|
|
1185
1192
|
): ModelManagerOptions<"openai-completions"> {
|
|
1186
|
-
|
|
1193
|
+
const apiKey = config?.apiKey;
|
|
1194
|
+
const baseUrl = config?.baseUrl ?? OPENGATEWAY_BASE_URL;
|
|
1195
|
+
const resolveReference = createReferenceResolver(createBundledReferenceMap<"openai-completions">("opengateway"));
|
|
1196
|
+
return {
|
|
1197
|
+
providerId: "opengateway",
|
|
1198
|
+
...(apiKey && {
|
|
1199
|
+
fetchDynamicModels: () =>
|
|
1200
|
+
fetchOpenAICompatibleModels({
|
|
1201
|
+
api: "openai-completions",
|
|
1202
|
+
provider: "opengateway",
|
|
1203
|
+
baseUrl,
|
|
1204
|
+
apiKey,
|
|
1205
|
+
mapModel: (entry, defaults) => {
|
|
1206
|
+
const reference =
|
|
1207
|
+
resolveReference(defaults.id) ??
|
|
1208
|
+
(defaults.id.endsWith(OPENGATEWAY_ULTRAFAST_SUFFIX)
|
|
1209
|
+
? resolveReference(defaults.id.slice(0, -OPENGATEWAY_ULTRAFAST_SUFFIX.length))
|
|
1210
|
+
: undefined);
|
|
1211
|
+
const mapped = mapWithBundledReference(entry, defaults, reference);
|
|
1212
|
+
return {
|
|
1213
|
+
...mapped,
|
|
1214
|
+
contextWindow: toPositiveNumber(entry.context_window, mapped.contextWindow),
|
|
1215
|
+
maxTokens: toPositiveNumber(entry.max_output_tokens, mapped.maxTokens),
|
|
1216
|
+
api: "openai-completions",
|
|
1217
|
+
provider: "opengateway",
|
|
1218
|
+
baseUrl,
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
}),
|
|
1222
|
+
}),
|
|
1223
|
+
};
|
|
1187
1224
|
}
|
|
1188
1225
|
|
|
1189
1226
|
// ---------------------------------------------------------------------------
|
|
@@ -107,7 +107,6 @@ export interface CpaToolAliasRestoreFailure {
|
|
|
107
107
|
*/
|
|
108
108
|
export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
|
|
109
109
|
export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
|
|
110
|
-
export declare const claudeCodeVersion = "2.1.281";
|
|
111
110
|
export declare const claudeCodeEntrypoint = "sdk-cli";
|
|
112
111
|
export declare const claudeToolPrefix: string;
|
|
113
112
|
export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
type ResolveToolChoiceResult,
|
|
114
114
|
resolveToolChoice,
|
|
115
115
|
} from "../utils/tool-choice-capability";
|
|
116
|
+
import { getClaudeCodeVersion } from "./claude-code-version";
|
|
116
117
|
import {
|
|
117
118
|
buildCopilotDynamicHeaders,
|
|
118
119
|
hasCopilotVisionInput,
|
|
@@ -294,7 +295,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
294
295
|
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
295
296
|
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
|
|
296
297
|
? incomingUserAgent
|
|
297
|
-
: `claude-cli/${
|
|
298
|
+
: `claude-cli/${getClaudeCodeVersion()} (external, cli)`;
|
|
298
299
|
return {
|
|
299
300
|
...modelHeaders,
|
|
300
301
|
...claudeCodeHeaders,
|
|
@@ -851,7 +852,6 @@ function getCacheControl(
|
|
|
851
852
|
}
|
|
852
853
|
|
|
853
854
|
// Stealth mode: Mimic Anthropic Code headers and tool prefixing.
|
|
854
|
-
export const claudeCodeVersion = "2.1.281";
|
|
855
855
|
export const claudeCodeEntrypoint = "sdk-cli";
|
|
856
856
|
export const claudeToolPrefix: string = "proxy_";
|
|
857
857
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
@@ -928,7 +928,7 @@ function createClaudeBillingHeader(payload: unknown): string {
|
|
|
928
928
|
const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
|
|
929
929
|
.join("")
|
|
930
930
|
.slice(0, 3);
|
|
931
|
-
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${
|
|
931
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${getClaudeCodeVersion()}.${buildHash}; cc_entrypoint=${claudeCodeEntrypoint}; cch=${cch};`;
|
|
932
932
|
}
|
|
933
933
|
|
|
934
934
|
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
@@ -1764,6 +1764,23 @@ function isTransientStreamEnvelopeError(error: unknown): boolean {
|
|
|
1764
1764
|
);
|
|
1765
1765
|
}
|
|
1766
1766
|
|
|
1767
|
+
/**
|
|
1768
|
+
* A request whose connection failed before the server returned any response:
|
|
1769
|
+
* the SDK's connection error, or a reset/closed/refused socket with no HTTP status.
|
|
1770
|
+
*/
|
|
1771
|
+
function isPreResponseConnectionFailure(error: unknown): boolean {
|
|
1772
|
+
if (!(error instanceof Error)) return false;
|
|
1773
|
+
if (extractHttpStatusFromError(error) !== undefined) return false;
|
|
1774
|
+
if (error instanceof Anthropic.APIConnectionTimeoutError) return false;
|
|
1775
|
+
if (error instanceof Anthropic.APIConnectionError) return true;
|
|
1776
|
+
const code = (error as { code?: unknown }).code;
|
|
1777
|
+
if (typeof code === "string" && /^(?:ECONNRESET|ECONNREFUSED|EPIPE|ENOTFOUND|EAI_AGAIN)$/.test(code)) return true;
|
|
1778
|
+
return (
|
|
1779
|
+
isUnexpectedSocketCloseMessage(error.message) ||
|
|
1780
|
+
/\b(?:ECONNRESET|ECONNREFUSED|EPIPE)\b|^connection error\.?$|other side closed/i.test(error.message)
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1767
1784
|
function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
|
|
1768
1785
|
if (!(error instanceof Error)) return false;
|
|
1769
1786
|
return /stream event order|before message_start/i.test(error.message);
|
|
@@ -2672,7 +2689,14 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2672
2689
|
// Otherwise the multi-megabyte body is re-uploaded up to the default
|
|
2673
2690
|
// streamMaxRetries budget despite the ceiling. Once iteration has
|
|
2674
2691
|
// begun, only the grace-clock path above decides.
|
|
2675
|
-
|
|
2692
|
+
// A connection that dropped before any response (reset, socket closed,
|
|
2693
|
+
// connect failure) is exempt: the server never answered, so a retry is
|
|
2694
|
+
// not a re-upload after a stall, and a network blip must not end the turn.
|
|
2695
|
+
if (
|
|
2696
|
+
requestUploadCeilingBound &&
|
|
2697
|
+
firstEventWaitStartedAt === undefined &&
|
|
2698
|
+
!isPreResponseConnectionFailure(streamFailure)
|
|
2699
|
+
) {
|
|
2676
2700
|
Object.assign(streamFailure as Error, {
|
|
2677
2701
|
requestBytes,
|
|
2678
2702
|
endpointClass,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Bundled floor; `scripts/check-spoofed-versions.ts --update` keeps it current. */
|
|
2
|
+
export declare const CLAUDE_CODE_BASELINE_VERSION = "2.1.284";
|
|
3
|
+
/** Pins the fingerprint and disables network resolution. */
|
|
4
|
+
export declare const CLAUDE_CODE_VERSION_ENV = "GJC_CLAUDE_CODE_VERSION";
|
|
5
|
+
/**
|
|
6
|
+
* Anthropic's native-installer release channel. `stable` deliberately lags the
|
|
7
|
+
* npm package during a staged rollout, so `latest` is the channel that matches
|
|
8
|
+
* what a freshly updated Claude Code actually reports.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLAUDE_RELEASE_CHANNEL_URL = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest";
|
|
11
|
+
export declare const CLAUDE_NPM_DIST_TAG_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
12
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
export declare function compareClaudeCodeVersions(a: string, b: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Latest published Claude Code version: the higher of the native-installer
|
|
16
|
+
* `latest` channel and the npm dist-tag, so one lagging or unavailable source
|
|
17
|
+
* cannot pin the fingerprint back. `null` when both sources fail.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchLatestClaudeCodeVersion(fetchImpl?: FetchLike, userAgent?: string): Promise<string | null>;
|
|
20
|
+
export interface ClaudeCodeVersionResolverOptions {
|
|
21
|
+
baseline: string;
|
|
22
|
+
/** Exact pin; disables network resolution when set. */
|
|
23
|
+
pinned?: string;
|
|
24
|
+
/** `null` disables both disk caching and network resolution. */
|
|
25
|
+
cachePath: string | null;
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
refreshIntervalMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ClaudeCodeVersionResolver {
|
|
31
|
+
/** Current fingerprint; schedules a background refresh when the cache is stale. */
|
|
32
|
+
get(): string;
|
|
33
|
+
/** Refresh now (deduplicated); resolves with the fingerprint afterwards. */
|
|
34
|
+
refresh(): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
export declare function createClaudeCodeVersionResolver(options: ClaudeCodeVersionResolverOptions): ClaudeCodeVersionResolver;
|
|
37
|
+
/** Claude Code version the `claude-cli` fingerprint and billing attribution sign with. */
|
|
38
|
+
export declare function getClaudeCodeVersion(): string;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code client-version resolution for the `claude-cli` fingerprint.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic gates newer models behind a minimum Claude Code version and answers
|
|
5
|
+
* stale fingerprints with HTTP 400 (`claude_code_version_too_old`). A version
|
|
6
|
+
* baked in at build time goes stale between releases, so the fingerprint is the
|
|
7
|
+
* higher of the bundled floor and the latest published Claude Code release,
|
|
8
|
+
* resolved in the background and cached on disk. Requests never wait on the
|
|
9
|
+
* network: they sign with whatever is known at that instant.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { getClaudeCodeVersionCachePath, isEnoent, logger } from "@gajae-code/utils";
|
|
14
|
+
|
|
15
|
+
/** Bundled floor; `scripts/check-spoofed-versions.ts --update` keeps it current. */
|
|
16
|
+
export const CLAUDE_CODE_BASELINE_VERSION = "2.1.284";
|
|
17
|
+
|
|
18
|
+
/** Pins the fingerprint and disables network resolution. */
|
|
19
|
+
export const CLAUDE_CODE_VERSION_ENV = "GJC_CLAUDE_CODE_VERSION";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Anthropic's native-installer release channel. `stable` deliberately lags the
|
|
23
|
+
* npm package during a staged rollout, so `latest` is the channel that matches
|
|
24
|
+
* what a freshly updated Claude Code actually reports.
|
|
25
|
+
*/
|
|
26
|
+
export const CLAUDE_RELEASE_CHANNEL_URL =
|
|
27
|
+
"https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest";
|
|
28
|
+
export const CLAUDE_NPM_DIST_TAG_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
29
|
+
|
|
30
|
+
const REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
31
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
32
|
+
const EXACT_SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
33
|
+
const EMBEDDED_SEMVER_RE = /(\d+\.\d+\.\d+)/;
|
|
34
|
+
|
|
35
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
36
|
+
|
|
37
|
+
export function compareClaudeCodeVersions(a: string, b: string): number {
|
|
38
|
+
const left = a.split(".").map(Number);
|
|
39
|
+
const right = b.split(".").map(Number);
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
42
|
+
if (diff !== 0) return diff;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isExactSemver(value: unknown): value is string {
|
|
48
|
+
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function highest(candidates: ReadonlyArray<string | null | undefined>): string | null {
|
|
52
|
+
let best: string | null = null;
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
if (!isExactSemver(candidate)) continue;
|
|
55
|
+
if (best === null || compareClaudeCodeVersions(candidate, best) > 0) best = candidate;
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchText(fetchImpl: FetchLike, url: string, userAgent: string): Promise<string | null> {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetchImpl(url, {
|
|
63
|
+
headers: { "User-Agent": userAgent },
|
|
64
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) return null;
|
|
67
|
+
return await res.text();
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchReleaseChannel(fetchImpl: FetchLike, userAgent: string): Promise<string | null> {
|
|
74
|
+
const text = await fetchText(fetchImpl, CLAUDE_RELEASE_CHANNEL_URL, userAgent);
|
|
75
|
+
return text === null ? null : (EMBEDDED_SEMVER_RE.exec(text.trim())?.[1] ?? null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function fetchNpmDistTag(fetchImpl: FetchLike, userAgent: string): Promise<string | null> {
|
|
79
|
+
const text = await fetchText(fetchImpl, CLAUDE_NPM_DIST_TAG_URL, userAgent);
|
|
80
|
+
if (text === null) return null;
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(text) as { version?: unknown };
|
|
83
|
+
return typeof data.version === "string" ? (EMBEDDED_SEMVER_RE.exec(data.version)?.[1] ?? null) : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Latest published Claude Code version: the higher of the native-installer
|
|
91
|
+
* `latest` channel and the npm dist-tag, so one lagging or unavailable source
|
|
92
|
+
* cannot pin the fingerprint back. `null` when both sources fail.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchLatestClaudeCodeVersion(
|
|
95
|
+
fetchImpl: FetchLike = fetch,
|
|
96
|
+
userAgent = "gajae-code/claude-code-version",
|
|
97
|
+
): Promise<string | null> {
|
|
98
|
+
return highest(
|
|
99
|
+
await Promise.all([fetchReleaseChannel(fetchImpl, userAgent), fetchNpmDistTag(fetchImpl, userAgent)]),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface CachedClaudeCodeVersion {
|
|
104
|
+
version: string;
|
|
105
|
+
checkedAt: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ClaudeCodeVersionResolverOptions {
|
|
109
|
+
baseline: string;
|
|
110
|
+
/** Exact pin; disables network resolution when set. */
|
|
111
|
+
pinned?: string;
|
|
112
|
+
/** `null` disables both disk caching and network resolution. */
|
|
113
|
+
cachePath: string | null;
|
|
114
|
+
fetch?: FetchLike;
|
|
115
|
+
now?: () => number;
|
|
116
|
+
refreshIntervalMs?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ClaudeCodeVersionResolver {
|
|
120
|
+
/** Current fingerprint; schedules a background refresh when the cache is stale. */
|
|
121
|
+
get(): string;
|
|
122
|
+
/** Refresh now (deduplicated); resolves with the fingerprint afterwards. */
|
|
123
|
+
refresh(): Promise<string>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createClaudeCodeVersionResolver(options: ClaudeCodeVersionResolverOptions): ClaudeCodeVersionResolver {
|
|
127
|
+
const { baseline, cachePath } = options;
|
|
128
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
129
|
+
const now = options.now ?? Date.now;
|
|
130
|
+
const refreshIntervalMs = options.refreshIntervalMs ?? REFRESH_INTERVAL_MS;
|
|
131
|
+
const pinned = isExactSemver(options.pinned) ? options.pinned : undefined;
|
|
132
|
+
|
|
133
|
+
let loaded = false;
|
|
134
|
+
let resolved: string | undefined;
|
|
135
|
+
let checkedAt = 0;
|
|
136
|
+
let inflight: Promise<string> | undefined;
|
|
137
|
+
|
|
138
|
+
const current = (): string => (resolved && compareClaudeCodeVersions(resolved, baseline) > 0 ? resolved : baseline);
|
|
139
|
+
|
|
140
|
+
const loadCache = (): void => {
|
|
141
|
+
if (loaded || cachePath === null) return;
|
|
142
|
+
loaded = true;
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(fs.readFileSync(cachePath, "utf-8")) as Partial<CachedClaudeCodeVersion>;
|
|
145
|
+
if (isExactSemver(data.version) && typeof data.checkedAt === "number" && Number.isFinite(data.checkedAt)) {
|
|
146
|
+
resolved = data.version;
|
|
147
|
+
checkedAt = data.checkedAt;
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (!isEnoent(error)) logger.debug("Ignoring unreadable Claude Code version cache", { error: String(error) });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const refresh = (): Promise<string> => {
|
|
155
|
+
if (pinned || cachePath === null) return Promise.resolve(pinned ?? current());
|
|
156
|
+
loadCache();
|
|
157
|
+
inflight ??= (async () => {
|
|
158
|
+
try {
|
|
159
|
+
const latest = await fetchLatestClaudeCodeVersion(fetchImpl);
|
|
160
|
+
// A failed lookup still counts as a check so an offline host does not
|
|
161
|
+
// retry on every request; the next attempt waits a full interval.
|
|
162
|
+
checkedAt = now();
|
|
163
|
+
if (latest && (!resolved || compareClaudeCodeVersions(latest, resolved) > 0)) resolved = latest;
|
|
164
|
+
if (resolved) {
|
|
165
|
+
const record: CachedClaudeCodeVersion = { version: resolved, checkedAt };
|
|
166
|
+
await Bun.write(cachePath, `${JSON.stringify(record)}\n`);
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.debug("Claude Code version refresh failed", { error: String(error) });
|
|
170
|
+
} finally {
|
|
171
|
+
inflight = undefined;
|
|
172
|
+
}
|
|
173
|
+
return current();
|
|
174
|
+
})();
|
|
175
|
+
return inflight;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
get() {
|
|
180
|
+
if (pinned) return pinned;
|
|
181
|
+
if (cachePath === null) return baseline;
|
|
182
|
+
loadCache();
|
|
183
|
+
if (!inflight && now() - checkedAt >= refreshIntervalMs) void refresh();
|
|
184
|
+
return current();
|
|
185
|
+
},
|
|
186
|
+
refresh,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let defaultResolver: ClaudeCodeVersionResolver | undefined;
|
|
191
|
+
|
|
192
|
+
function getDefaultResolver(): ClaudeCodeVersionResolver {
|
|
193
|
+
defaultResolver ??= createClaudeCodeVersionResolver({
|
|
194
|
+
baseline: CLAUDE_CODE_BASELINE_VERSION,
|
|
195
|
+
pinned: process.env[CLAUDE_CODE_VERSION_ENV]?.trim() || undefined,
|
|
196
|
+
// Tests must stay hermetic and deterministic: no network, no user cache.
|
|
197
|
+
cachePath: process.env.NODE_ENV === "test" ? null : path.resolve(getClaudeCodeVersionCachePath()),
|
|
198
|
+
});
|
|
199
|
+
return defaultResolver;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Claude Code version the `claude-cli` fingerprint and billing attribution sign with. */
|
|
203
|
+
export function getClaudeCodeVersion(): string {
|
|
204
|
+
return getDefaultResolver().get();
|
|
205
|
+
}
|
|
@@ -58,11 +58,15 @@ interface WireToolSpec {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
interface WireToolResult {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
toolUseId: string;
|
|
62
|
+
status: "success" | "error";
|
|
63
|
+
content: Array<{ text: string }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface WireToolUse {
|
|
67
|
+
toolUseId: string;
|
|
68
|
+
name: string;
|
|
69
|
+
input: Record<string, unknown>;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
interface WireUserMessage {
|
|
@@ -71,7 +75,7 @@ interface WireUserMessage {
|
|
|
71
75
|
modelId?: string;
|
|
72
76
|
userInputMessageContext?: {
|
|
73
77
|
tools?: { tools: WireToolSpec[] };
|
|
74
|
-
toolResults?:
|
|
78
|
+
toolResults?: WireToolResult[];
|
|
75
79
|
editorStateContext?: Record<string, unknown>;
|
|
76
80
|
};
|
|
77
81
|
origin?: string;
|
|
@@ -81,6 +85,7 @@ interface WireUserMessage {
|
|
|
81
85
|
interface WireAssistantMessage {
|
|
82
86
|
assistantResponseMessage: {
|
|
83
87
|
content: string;
|
|
88
|
+
toolUses?: WireToolUse[];
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -383,6 +388,21 @@ function buildConversationState(
|
|
|
383
388
|
|
|
384
389
|
for (let i = 0; i < messages.length - 1; i++) {
|
|
385
390
|
const msg = messages[i];
|
|
391
|
+
const previous = history[history.length - 1];
|
|
392
|
+
// Parallel tool calls produce consecutive toolResult messages; they answer one
|
|
393
|
+
// assistant turn, so their results share a single user entry.
|
|
394
|
+
if (
|
|
395
|
+
msg.role === "toolResult" &&
|
|
396
|
+
messages[i - 1]?.role === "toolResult" &&
|
|
397
|
+
previous &&
|
|
398
|
+
"userInputMessage" in previous
|
|
399
|
+
) {
|
|
400
|
+
previous.userInputMessage.userInputMessageContext ??= {};
|
|
401
|
+
const context = previous.userInputMessage.userInputMessageContext;
|
|
402
|
+
context.toolResults ??= [];
|
|
403
|
+
context.toolResults.push(convertToolResult(msg as ToolResultMessage));
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
386
406
|
history.push(convertToWireMessage(msg, modelId, i === 0 ? systemPrompt : undefined));
|
|
387
407
|
}
|
|
388
408
|
|
|
@@ -419,20 +439,27 @@ function convertToWireMessage(
|
|
|
419
439
|
if (msg.role === "toolResult") {
|
|
420
440
|
return convertToWireUserMessage(msg, modelId, systemPrompt);
|
|
421
441
|
}
|
|
422
|
-
// assistant → assistant response
|
|
442
|
+
// assistant → assistant response; tool calls stay structured so later tool
|
|
443
|
+
// results can reference the toolUseId they answer.
|
|
423
444
|
const textParts: string[] = [];
|
|
445
|
+
const toolUses: WireToolUse[] = [];
|
|
424
446
|
for (const block of msg.content) {
|
|
425
447
|
if (typeof block === "string") {
|
|
426
448
|
textParts.push(block);
|
|
427
449
|
} else if (block.type === "text") {
|
|
428
450
|
textParts.push(block.text);
|
|
429
451
|
} else if (block.type === "toolCall") {
|
|
430
|
-
|
|
452
|
+
toolUses.push({
|
|
453
|
+
toolUseId: block.id,
|
|
454
|
+
name: block.name,
|
|
455
|
+
input: (block.arguments ?? {}) as Record<string, unknown>,
|
|
456
|
+
});
|
|
431
457
|
}
|
|
432
458
|
}
|
|
433
459
|
return {
|
|
434
460
|
assistantResponseMessage: {
|
|
435
|
-
content: textParts.join("\n")
|
|
461
|
+
content: textParts.join("\n"),
|
|
462
|
+
...(toolUses.length > 0 ? { toolUses } : {}),
|
|
436
463
|
},
|
|
437
464
|
};
|
|
438
465
|
}
|
|
@@ -442,7 +469,9 @@ function convertToWireUserMessage(
|
|
|
442
469
|
modelId: string,
|
|
443
470
|
systemPrompt?: string,
|
|
444
471
|
): WireUserMessage {
|
|
445
|
-
|
|
472
|
+
// A tool result travels in userInputMessageContext.toolResults; repeating it as
|
|
473
|
+
// message text would show the model each result twice.
|
|
474
|
+
let content = msg.role === "toolResult" ? "Tool results provided." : extractTextContent(msg);
|
|
446
475
|
if (systemPrompt) {
|
|
447
476
|
content = `${systemPrompt}\n\n${content}`;
|
|
448
477
|
}
|
|
@@ -454,27 +483,28 @@ function convertToWireUserMessage(
|
|
|
454
483
|
},
|
|
455
484
|
};
|
|
456
485
|
|
|
457
|
-
// Handle tool results
|
|
458
486
|
if (msg.role === "toolResult") {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
content: detail.type === "text" ? detail.text : "",
|
|
463
|
-
toolUseId: toolResultMsg.toolCallId,
|
|
464
|
-
status: toolResultMsg.isError ? ("error" as const) : ("success" as const),
|
|
465
|
-
},
|
|
466
|
-
}));
|
|
467
|
-
if (toolResults.length > 0) {
|
|
468
|
-
if (!userMsg.userInputMessage.userInputMessageContext) {
|
|
469
|
-
userMsg.userInputMessage.userInputMessageContext = {};
|
|
470
|
-
}
|
|
471
|
-
userMsg.userInputMessage.userInputMessageContext.toolResults = { toolResults: [toolResults] };
|
|
472
|
-
}
|
|
487
|
+
userMsg.userInputMessage.userInputMessageContext = {
|
|
488
|
+
toolResults: [convertToolResult(msg as ToolResultMessage)],
|
|
489
|
+
};
|
|
473
490
|
}
|
|
474
491
|
|
|
475
492
|
return userMsg;
|
|
476
493
|
}
|
|
477
494
|
|
|
495
|
+
/** One CodeWhisperer tool result per tool call, keyed by the toolUseId it answers. */
|
|
496
|
+
function convertToolResult(msg: ToolResultMessage): WireToolResult {
|
|
497
|
+
const text = (msg.content ?? [])
|
|
498
|
+
.map(block => (block.type === "text" ? block.text : block.type === "image" ? "[image omitted]" : ""))
|
|
499
|
+
.filter(part => part.length > 0)
|
|
500
|
+
.join("\n");
|
|
501
|
+
return {
|
|
502
|
+
toolUseId: msg.toolCallId,
|
|
503
|
+
status: msg.isError ? "error" : "success",
|
|
504
|
+
content: [{ text }],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
478
508
|
function extractTextContent(msg: Context["messages"][number]): string {
|
|
479
509
|
if (typeof msg.content === "string") return msg.content;
|
|
480
510
|
if (Array.isArray(msg.content)) {
|
|
@@ -222,6 +222,14 @@ function isCodexStreamProgressEvent(event: unknown): boolean {
|
|
|
222
222
|
const delta = (event as { delta?: unknown }).delta;
|
|
223
223
|
return typeof delta === "string" && delta.length > 0;
|
|
224
224
|
}
|
|
225
|
+
|
|
226
|
+
function codexOutputHasMeaningfulProgress(output: AssistantMessage): boolean {
|
|
227
|
+
return output.content.some(
|
|
228
|
+
block =>
|
|
229
|
+
(block.type === "text" && block.text.length > 0) || (block.type === "thinking" && block.thinking.length > 0),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
type CodexTransport = "sse" | "websocket";
|
|
226
234
|
interface CodexInitialTransport {
|
|
227
235
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
@@ -412,7 +420,7 @@ function getCodexWebSocketIdleTimeoutMs(overrideMs?: number): number {
|
|
|
412
420
|
overrideMs ??
|
|
413
421
|
parseCodexPositiveInteger(
|
|
414
422
|
$env.GJC_OPENAI_CODE_WEBSOCKET_IDLE_TIMEOUT_MS ?? $env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS,
|
|
415
|
-
CODEX_WEBSOCKET_IDLE_TIMEOUT_MS,
|
|
423
|
+
getOpenAIStreamIdleTimeoutMs() ?? CODEX_WEBSOCKET_IDLE_TIMEOUT_MS,
|
|
416
424
|
)
|
|
417
425
|
);
|
|
418
426
|
}
|
|
@@ -1728,6 +1736,20 @@ async function recoverCodexStreamError(
|
|
|
1728
1736
|
error: unknown,
|
|
1729
1737
|
): Promise<boolean> {
|
|
1730
1738
|
if (isCodexFirstEventTimeout(error)) return false;
|
|
1739
|
+
// A post-progress idle timeout has already consumed the stream's silence budget.
|
|
1740
|
+
// Replaying it over another transport can leave the turn silent for another full window.
|
|
1741
|
+
if (
|
|
1742
|
+
(codexOutputHasMeaningfulProgress(context.output) ||
|
|
1743
|
+
(context.output.content.some(block => block.type === "thinking") &&
|
|
1744
|
+
context.output.content.some(
|
|
1745
|
+
block => block.type === "toolCall" && !runtime.finalizedToolCallIds.has(block.id),
|
|
1746
|
+
))) &&
|
|
1747
|
+
error instanceof Error &&
|
|
1748
|
+
isCodexWebSocketTransportError(error) &&
|
|
1749
|
+
error.message.includes("idle timeout waiting for websocket")
|
|
1750
|
+
) {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1731
1753
|
if (await tryRetryWithoutForcedToolChoice(context, runtime, error)) {
|
|
1732
1754
|
return true;
|
|
1733
1755
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
2
|
import { $credentialEnv, $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
|
|
3
|
-
import OpenAI, { APIConnectionTimeoutError } from "openai";
|
|
3
|
+
import OpenAI, { APIConnectionTimeoutError, type ClientOptions as OpenAIClientOptions } from "openai";
|
|
4
4
|
import type {
|
|
5
5
|
ChatCompletionAssistantMessageParam,
|
|
6
6
|
ChatCompletionChunk,
|
|
@@ -1509,6 +1509,22 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
1509
1509
|
return stream;
|
|
1510
1510
|
};
|
|
1511
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* The OpenAI SDK retries pre-header timeouts and connection failures silently
|
|
1514
|
+
* (its retry lines log at `info`, below the default `warn`, and go to console,
|
|
1515
|
+
* which corrupts the TUI). Route them into the gjc log so a request stuck
|
|
1516
|
+
* before response headers — where our stream watchdogs are not armed yet —
|
|
1517
|
+
* leaves a trace instead of looking like a hung turn.
|
|
1518
|
+
*/
|
|
1519
|
+
const OPENAI_SDK_LOGGER: NonNullable<OpenAIClientOptions["logger"]> = {
|
|
1520
|
+
error: message => logger.warn(`openai-sdk: ${message}`),
|
|
1521
|
+
warn: message => logger.warn(`openai-sdk: ${message}`),
|
|
1522
|
+
info: message => {
|
|
1523
|
+
if (/retrying|timed out|connection failed/.test(message)) logger.warn(`openai-sdk: ${message}`);
|
|
1524
|
+
},
|
|
1525
|
+
debug: () => {},
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1512
1528
|
async function createClient(
|
|
1513
1529
|
model: Model<"openai-completions">,
|
|
1514
1530
|
context: Context,
|
|
@@ -1680,6 +1696,8 @@ async function createClient(
|
|
|
1680
1696
|
defaultHeaders: headers,
|
|
1681
1697
|
fetch: debugFetch,
|
|
1682
1698
|
...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
|
|
1699
|
+
logLevel: "info",
|
|
1700
|
+
logger: OPENAI_SDK_LOGGER,
|
|
1683
1701
|
}),
|
|
1684
1702
|
copilotPremiumRequests,
|
|
1685
1703
|
baseUrl,
|
package/src/usage/claude.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { getClaudeCodeVersion } from "../providers/claude-code-version";
|
|
3
3
|
import type {
|
|
4
4
|
CredentialRankingStrategy,
|
|
5
5
|
UsageAmount,
|
|
@@ -32,7 +32,6 @@ const CLAUDE_HEADERS = {
|
|
|
32
32
|
"anthropic-beta":
|
|
33
33
|
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
|
34
34
|
"content-type": "application/json",
|
|
35
|
-
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
36
35
|
connection: "keep-alive",
|
|
37
36
|
} as const;
|
|
38
37
|
|
|
@@ -352,6 +351,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
352
351
|
const url = `${baseUrl}/usage`;
|
|
353
352
|
const headers: Record<string, string> = {
|
|
354
353
|
...CLAUDE_HEADERS,
|
|
354
|
+
"user-agent": `claude-cli/${getClaudeCodeVersion()} (external, cli)`,
|
|
355
355
|
authorization: `Bearer ${credential.accessToken}`,
|
|
356
356
|
};
|
|
357
357
|
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
|
|
3
|
-
* HTTP/1.1 fallback when the server doesn't negotiate `h2`.
|
|
4
|
-
*
|
|
5
|
-
* Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
|
|
6
|
-
* read by the native runtime before any JS executes; assigning to
|
|
7
|
-
* `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
|
|
8
|
-
* activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
|
|
9
|
-
* if the server picks anything else, so we catch and retry without the hint.
|
|
10
|
-
*
|
|
11
|
-
* Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
|
|
12
|
-
* advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
|
|
13
|
-
* framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
|
|
14
|
-
* `ConnectionClosed`, or `HTTP2StreamReset` rather than `HTTP2Unsupported`.
|
|
15
|
-
* `ConnectionRefused` is raised before the request is written, while
|
|
16
|
-
* `HTTP2RefusedStream` is the explicit HTTP/2 promise that a stream was never
|
|
17
|
-
* processed (including a stream above a graceful GOAWAY last-stream-id).
|
|
18
|
-
* `ConnectionReset`, `ConnectionClosed`, and a generic `HTTP2StreamReset` do
|
|
19
|
-
* not carry that promise: the peer may have consumed the body before the
|
|
20
|
-
* connection failed. Retrying those codes would duplicate non-idempotent side
|
|
21
|
-
* effects, so they preserve the original error instead of falling back.
|
|
22
|
-
*
|
|
23
|
-
* ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
|
|
24
|
-
* the TLS handshake entirely when the client offers ALPN h2. Bun reports that
|
|
25
|
-
* abort as `UNKNOWN_CERTIFICATE_VERIFICATION_ERROR` even though the host's
|
|
26
|
-
* certificate chain verifies fine over h1 (issue #5178), so that code is a
|
|
27
|
-
* fallback trigger too — never a reason to accept a bad certificate: the h1
|
|
28
|
-
* attempt below performs full verification on its own.
|
|
29
|
-
*
|
|
30
|
-
* Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
|
|
31
|
-
* skip the attempt entirely — avoids the throw/retry round-trip for localhost.
|
|
32
|
-
*
|
|
33
|
-
* Idempotent.
|
|
34
|
-
*/
|
|
35
|
-
export declare function installH2Fetch(): void;
|
package/src/utils/h2-fetch.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
|
|
3
|
-
* HTTP/1.1 fallback when the server doesn't negotiate `h2`.
|
|
4
|
-
*
|
|
5
|
-
* Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
|
|
6
|
-
* read by the native runtime before any JS executes; assigning to
|
|
7
|
-
* `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
|
|
8
|
-
* activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
|
|
9
|
-
* if the server picks anything else, so we catch and retry without the hint.
|
|
10
|
-
*
|
|
11
|
-
* Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
|
|
12
|
-
* advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
|
|
13
|
-
* framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
|
|
14
|
-
* `ConnectionClosed`, or `HTTP2StreamReset` rather than `HTTP2Unsupported`.
|
|
15
|
-
* `ConnectionRefused` is raised before the request is written, while
|
|
16
|
-
* `HTTP2RefusedStream` is the explicit HTTP/2 promise that a stream was never
|
|
17
|
-
* processed (including a stream above a graceful GOAWAY last-stream-id).
|
|
18
|
-
* `ConnectionReset`, `ConnectionClosed`, and a generic `HTTP2StreamReset` do
|
|
19
|
-
* not carry that promise: the peer may have consumed the body before the
|
|
20
|
-
* connection failed. Retrying those codes would duplicate non-idempotent side
|
|
21
|
-
* effects, so they preserve the original error instead of falling back.
|
|
22
|
-
*
|
|
23
|
-
* ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
|
|
24
|
-
* the TLS handshake entirely when the client offers ALPN h2. Bun reports that
|
|
25
|
-
* abort as `UNKNOWN_CERTIFICATE_VERIFICATION_ERROR` even though the host's
|
|
26
|
-
* certificate chain verifies fine over h1 (issue #5178), so that code is a
|
|
27
|
-
* fallback trigger too — never a reason to accept a bad certificate: the h1
|
|
28
|
-
* attempt below performs full verification on its own.
|
|
29
|
-
*
|
|
30
|
-
* Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
|
|
31
|
-
* skip the attempt entirely — avoids the throw/retry round-trip for localhost.
|
|
32
|
-
*
|
|
33
|
-
* Idempotent.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
const installed: unique symbol = Symbol.for("gajae-code.h2fetch.installed");
|
|
37
|
-
|
|
38
|
-
interface PatchedFetch {
|
|
39
|
-
[installed]?: true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function installH2Fetch(): void {
|
|
43
|
-
const original = globalThis.fetch as typeof fetch & PatchedFetch;
|
|
44
|
-
if (original[installed]) return;
|
|
45
|
-
|
|
46
|
-
/** Error codes that indicate h2 negotiation/transport failure (not an application error). */
|
|
47
|
-
const h2FallbackCodes: ReadonlySet<string> = new Set([
|
|
48
|
-
"HTTP2Unsupported", // Server selected h1 in ALPN
|
|
49
|
-
"ConnectionRefused", // Server refused the h2 connection
|
|
50
|
-
"HTTP2RefusedStream", // REFUSED_STREAM / never-processed h2 stream
|
|
51
|
-
// Bun's h2 client reports an ALPN-refusing host's TLS abort with this
|
|
52
|
-
// code; the h1 fallback below re-verifies the certificate itself.
|
|
53
|
-
"UNKNOWN_CERTIFICATE_VERIFICATION_ERROR",
|
|
54
|
-
]);
|
|
55
|
-
const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
56
|
-
if (!isHttps(input)) return original(input, init);
|
|
57
|
-
const snapshot = snapshotRequest(input, init);
|
|
58
|
-
try {
|
|
59
|
-
return await original(snapshot.input, { ...snapshot.init, protocol: "http2" });
|
|
60
|
-
} catch (err) {
|
|
61
|
-
const code = (err as { code?: string }).code ?? "";
|
|
62
|
-
if (!h2FallbackCodes.has(code)) throw err;
|
|
63
|
-
if (code === "HTTP2RefusedStream" && !isReplayableRequest(snapshot.input, snapshot.init)) throw err;
|
|
64
|
-
return original(snapshot.input, snapshot.init);
|
|
65
|
-
}
|
|
66
|
-
} as typeof fetch & PatchedFetch;
|
|
67
|
-
|
|
68
|
-
// Preserve `fetch.preconnect` and any other statics SDK code might poke at.
|
|
69
|
-
Object.assign(wrapper, original);
|
|
70
|
-
wrapper[installed] = true;
|
|
71
|
-
globalThis.fetch = wrapper;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function snapshotRequest(
|
|
75
|
-
input: string | URL | Request,
|
|
76
|
-
init?: RequestInit,
|
|
77
|
-
): { input: string | URL | Request; init?: RequestInit } {
|
|
78
|
-
const snapshotInput = input instanceof URL ? new URL(input.href) : input;
|
|
79
|
-
const requestLike = typeof input !== "string" && !(input instanceof URL);
|
|
80
|
-
if (init === undefined && !requestLike) {
|
|
81
|
-
return { input: snapshotInput };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const snapshotInit: RequestInit = { ...(init ?? {}) };
|
|
85
|
-
if (init?.headers !== undefined) {
|
|
86
|
-
snapshotInit.headers = snapshotHeaders(init.headers);
|
|
87
|
-
} else if (requestLike) {
|
|
88
|
-
snapshotInit.headers = new Headers(input.headers);
|
|
89
|
-
}
|
|
90
|
-
return { input: snapshotInput, init: snapshotInit };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function snapshotHeaders(headers: NonNullable<RequestInit["headers"]>): NonNullable<RequestInit["headers"]> {
|
|
94
|
-
if (headers instanceof Headers) return new Headers(headers);
|
|
95
|
-
if (Array.isArray(headers)) {
|
|
96
|
-
return headers.map(([name, value]) => [name, value] as [string, string]);
|
|
97
|
-
}
|
|
98
|
-
return { ...headers };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function isHttps(input: string | URL | Request): boolean {
|
|
102
|
-
if (typeof input === "string") return input.startsWith("https:");
|
|
103
|
-
if (input instanceof URL) return input.protocol === "https:";
|
|
104
|
-
return input.url.startsWith("https:");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Whether the request body can be handed to fetch a second time. This is only
|
|
109
|
-
* used after `HTTP2RefusedStream`, which proves that the peer did not consume
|
|
110
|
-
* request bytes; a one-shot ReadableStream is still not reusable locally.
|
|
111
|
-
*/
|
|
112
|
-
function isReplayableRequest(input: string | URL | Request, init?: RequestInit): boolean {
|
|
113
|
-
try {
|
|
114
|
-
if (init?.body !== undefined) return isReplayableBody(init.body);
|
|
115
|
-
if (typeof input === "string" || input instanceof URL) return true;
|
|
116
|
-
// Request bodies are exposed as ReadableStreams. The same Request object
|
|
117
|
-
// cannot be used for the second fetch once the first attempt touched it.
|
|
118
|
-
return input.body === null;
|
|
119
|
-
} catch {
|
|
120
|
-
// Cross-realm or proxy Request objects may throw while exposing their
|
|
121
|
-
// method/body. Do not retry when replayability cannot be established.
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isReplayableBody(body: RequestInit["body"]): boolean {
|
|
127
|
-
if (body === null || typeof body === "string") return true;
|
|
128
|
-
if (typeof Blob !== "undefined" && body instanceof Blob) return true;
|
|
129
|
-
// ArrayBuffer/views, FormData, and URLSearchParams are mutable caller-owned
|
|
130
|
-
// values. Fail closed instead of replaying a potentially changed payload if
|
|
131
|
-
// the asynchronous refusal arrives after the caller mutates one.
|
|
132
|
-
return false;
|
|
133
|
-
}
|