@gajae-code/ai 0.18.1 → 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 CHANGED
@@ -2,6 +2,32 @@
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
+
5
31
  ## [0.18.1] - 2026-09-29
6
32
 
7
33
  ### Changed
@@ -46,7 +46,6 @@ export * from "./utils/anthropic-auth";
46
46
  export * from "./utils/discovery";
47
47
  export * from "./utils/event-stream";
48
48
  export * from "./utils/fallback-transport";
49
- export * from "./utils/h2-fetch";
50
49
  export * from "./utils/oauth";
51
50
  export type { OAuthCredentials, OAuthLoginOptions, OAuthProvider, OAuthProviderId, OAuthProviderInfo, } from "./utils/oauth/types";
52
51
  export * from "./utils/overflow";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.18.1",
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.1",
45
- "@gajae-code/utils": "0.18.1",
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
@@ -46,7 +46,6 @@ export * from "./utils/anthropic-auth";
46
46
  export * from "./utils/discovery";
47
47
  export * from "./utils/event-stream";
48
48
  export * from "./utils/fallback-transport";
49
- export * from "./utils/h2-fetch";
50
49
  export * from "./utils/oauth";
51
50
  export type {
52
51
  OAuthCredentials,
@@ -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",
@@ -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
- if (requestUploadCeilingBound && firstEventWaitStartedAt === undefined) {
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,
@@ -58,11 +58,15 @@ interface WireToolSpec {
58
58
  }
59
59
 
60
60
  interface WireToolResult {
61
- toolResultMessage: {
62
- content: string;
63
- toolUseId: string;
64
- status?: "success" | "error";
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?: { toolResults: WireToolResult[][] };
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
- textParts.push(JSON.stringify({ toolUseId: block.id, name: block.name, input: block.arguments }));
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
- let content = extractTextContent(msg);
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
- const toolResultMsg = msg as ToolResultMessage;
460
- const toolResults = (toolResultMsg.content ?? []).map(detail => ({
461
- toolResultMessage: {
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)) {
@@ -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,
@@ -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;
@@ -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
- }