@gajae-code/ai 0.16.1 → 0.16.4

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/auth-gateway/server.d.ts +4 -2
  3. package/dist/types/bedrock-claude-cache-policy.d.ts +21 -0
  4. package/dist/types/openai-completions-compat.d.ts +22 -0
  5. package/dist/types/providers/amazon-bedrock.d.ts +17 -2
  6. package/dist/types/providers/anthropic.d.ts +1 -0
  7. package/dist/types/providers/openai-completions-compat.d.ts +1 -1
  8. package/dist/types/providers/opencode-go-session.d.ts +5 -0
  9. package/dist/types/types.d.ts +7 -0
  10. package/dist/types/utils/codex-entitlement.d.ts +22 -0
  11. package/package.json +3 -3
  12. package/src/auth-gateway/server.ts +5 -0
  13. package/src/auth-storage.ts +31 -8
  14. package/src/bedrock-claude-cache-policy.d.ts +21 -0
  15. package/src/bedrock-claude-cache-policy.ts +68 -0
  16. package/src/model-pricing.ts +11 -0
  17. package/src/model-thinking.ts +31 -7
  18. package/src/models.json +37 -0
  19. package/src/openai-completions-compat.d.ts +22 -0
  20. package/src/openai-completions-compat.ts +77 -3
  21. package/src/providers/amazon-bedrock.d.ts +17 -2
  22. package/src/providers/amazon-bedrock.ts +9 -9
  23. package/src/providers/anthropic.d.ts +1 -0
  24. package/src/providers/anthropic.ts +38 -21
  25. package/src/providers/cursor.ts +12 -6
  26. package/src/providers/openai-codex/response-handler.ts +1 -1
  27. package/src/providers/openai-codex-responses.ts +14 -5
  28. package/src/providers/openai-completions-compat.d.ts +1 -1
  29. package/src/providers/openai-completions-compat.ts +8 -1
  30. package/src/providers/openai-completions.ts +14 -1
  31. package/src/providers/openai-responses.ts +11 -1
  32. package/src/providers/opencode-go-session.d.ts +5 -0
  33. package/src/providers/opencode-go-session.ts +57 -0
  34. package/src/stream.ts +1 -0
  35. package/src/types.d.ts +7 -0
  36. package/src/types.ts +7 -0
  37. package/src/utils/codex-entitlement.d.ts +22 -0
  38. package/src/utils/codex-entitlement.ts +57 -0
  39. package/src/utils/discovery/antigravity.ts +2 -1
  40. package/src/utils/discovery/gemini.ts +2 -1
@@ -48,10 +48,57 @@ function parseHostname(baseUrl: string): string | undefined {
48
48
  }
49
49
  }
50
50
 
51
+ function isProductionXaiApiUrl(baseUrl: string): boolean {
52
+ if (
53
+ /[\u0000-\u0020\u007f]/u.test(baseUrl) ||
54
+ baseUrl.includes("?") ||
55
+ baseUrl.includes("#") ||
56
+ baseUrl.includes("\\")
57
+ ) {
58
+ return false;
59
+ }
60
+ const authorityTail = baseUrl.slice("https://".length);
61
+ const authorityEnd = authorityTail.search(/[/?#]/u);
62
+ const authority = authorityTail.slice(0, authorityEnd === -1 ? undefined : authorityEnd);
63
+ if (!/^api\.x\.ai(?::443)?$/iu.test(authority)) return false;
64
+ try {
65
+ const url = new URL(baseUrl);
66
+ return (
67
+ url.protocol === "https:" &&
68
+ url.hostname.toLowerCase() === "api.x.ai" &&
69
+ url.port === "" &&
70
+ url.username === "" &&
71
+ url.password === "" &&
72
+ url.search === "" &&
73
+ url.hash === ""
74
+ );
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
51
80
  function hostnameMatches(hostname: string | undefined, suffix: string): boolean {
52
81
  return hostname !== undefined && (hostname === suffix || hostname.endsWith(`.${suffix}`));
53
82
  }
54
83
 
84
+ export type GrokGeneration = {
85
+ major: number;
86
+ minor: number;
87
+ };
88
+
89
+ function parseGrokGeneration(modelId: string): GrokGeneration | undefined {
90
+ const canonicalId = modelId.startsWith("x-ai/") ? modelId.slice("x-ai/".length) : modelId;
91
+ const match =
92
+ /^grok-([1-9]\d?)(?:\.(0|[1-9]\d?))?(?:-(latest|preview|\d{8}|reasoning|\d{4}-reasoning|beta-latest-reasoning|multi-agent-beta-latest))?$/.exec(
93
+ canonicalId,
94
+ );
95
+ if (!match) return undefined;
96
+ return {
97
+ major: Number(match[1]),
98
+ minor: match[2] === undefined ? 0 : Number(match[2]),
99
+ };
100
+ }
101
+
55
102
  /** Returns whether the request endpoint is an audited reasoning-control transport. */
56
103
  export function isAuditedOpenAIReasoningTransport(
57
104
  model: { provider: string; baseUrl?: string },
@@ -63,6 +110,32 @@ export function isAuditedOpenAIReasoningTransport(
63
110
  return AUDITED_REASONING_EFFORT_HOST_SUFFIXES.some(suffix => hostnameMatches(hostname, suffix));
64
111
  }
65
112
 
113
+ /**
114
+ * xAI's first-party API accepts `reasoning_effort` on Grok 4.5 and later.
115
+ * Provider labels are user-configurable, so both the provider and the official
116
+ * API origin must match. Unknown variants fail closed instead of inheriting a
117
+ * capability from a loose model-id prefix.
118
+ */
119
+ export function parseDirectXaiReasoningEffortGeneration(
120
+ model: { provider: string; id: string; api?: string; baseUrl?: string },
121
+ resolvedBaseUrl?: string,
122
+ ): GrokGeneration | undefined {
123
+ if (model.provider !== "xai") return undefined;
124
+ if (model.api !== "openai-completions") return undefined;
125
+ if (!isProductionXaiApiUrl(resolvedBaseUrl ?? model.baseUrl ?? "")) return undefined;
126
+ if (model.id.includes("/")) return undefined;
127
+ const generation = parseGrokGeneration(model.id);
128
+ if (!generation) return undefined;
129
+ return generation.major > 4 || (generation.major === 4 && generation.minor >= 5) ? generation : undefined;
130
+ }
131
+
132
+ export function isDirectXaiReasoningEffortModel(
133
+ model: { provider: string; id: string; api?: string; baseUrl?: string },
134
+ resolvedBaseUrl?: string,
135
+ ): boolean {
136
+ return parseDirectXaiReasoningEffortGeneration(model, resolvedBaseUrl) !== undefined;
137
+ }
138
+
66
139
  export type ResolvedOpenAICompat = Required<
67
140
  Omit<
68
141
  OpenAICompat,
@@ -184,8 +257,8 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
184
257
  baseUrl.includes("chutes.ai") ||
185
258
  baseUrl.includes("fireworks.ai") ||
186
259
  isDirectDeepseekApi;
187
- const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
188
- const isDirectXaiReasoningEffortModel = provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6");
260
+ const isGrok = provider === "xai" || baseUrl.includes("api.x.ai") || parseGrokGeneration(model.id) !== undefined;
261
+
189
262
  const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai");
190
263
 
191
264
  // Hosts whose chat-completions endpoints are known to accept multiple
@@ -278,7 +351,8 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
278
351
  supportsResponsesSessionAffinity: false,
279
352
  supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
280
353
  supportsReasoningEffort:
281
- hasAuditedReasoningEffortTransport && ((!isGrok && !isZai) || isDirectXaiReasoningEffortModel),
354
+ hasAuditedReasoningEffortTransport &&
355
+ ((!isGrok && !isZai) || isOpenRouter || isDirectXaiReasoningEffortModel(model, resolvedBaseUrl)),
282
356
  reasoningEffortMap,
283
357
  supportsUsageInStreaming: !isCerebras,
284
358
  disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel || isOpenCodeGoReasoning,
@@ -6,8 +6,9 @@
6
6
  * No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
7
7
  * Bun's native `HTTPS_PROXY` support.
8
8
  */
9
+ import { parseBedrockClaudeGeneration } from "../bedrock-claude-cache-policy";
9
10
  import type { Effort } from "../model-thinking";
10
- import type { StreamFunction, StreamOptions, ThinkingBudgets, Tool, ToolChoice } from "../types";
11
+ import type { Model, StreamFunction, StreamOptions, ThinkingBudgets, Tool, ToolChoice } from "../types";
11
12
  export type BedrockThinkingDisplay = "summarized" | "omitted";
12
13
  export interface BedrockOptions extends StreamOptions {
13
14
  region?: string;
@@ -51,10 +52,24 @@ interface WireToolConfig {
51
52
  toolChoice?: WireToolChoice;
52
53
  }
53
54
  export declare const streamBedrock: StreamFunction<"bedrock-converse-stream">;
55
+ /**
56
+ * Check if the model supports prompt caching.
57
+ * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, and every later Claude
58
+ * generation:
59
+ * https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
60
+ *
61
+ * For base models and system-defined inference profiles the model ID / ARN
62
+ * contains the model name, so we can decide locally.
63
+ *
64
+ * For application inference profiles (whose ARNs don't contain the model name),
65
+ * set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. Amazon Nova models
66
+ * have automatic caching and don't need explicit cache points.
67
+ */
68
+ export declare function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean;
69
+ export { parseBedrockClaudeGeneration };
54
70
  export declare function stripBedrockForcedToolChoiceForRetry<T extends {
55
71
  toolConfig?: {
56
72
  toolChoice?: unknown;
57
73
  };
58
74
  }>(body: T): T;
59
75
  export declare function convertToolConfig(tools: Tool[] | undefined, toolChoice: BedrockOptions["toolChoice"]): WireToolConfig | undefined;
60
- export {};
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { $credentialEnv, $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@gajae-code/utils";
11
11
  import { assertAwsRegionLabel } from "../adapter-internals/aws-region";
12
+ import { parseBedrockClaudeGeneration, supportsBedrockClaudePromptCaching } from "../bedrock-claude-cache-policy";
12
13
  import type { Effort } from "../model-thinking";
13
14
  import {
14
15
  mapEffortToAnthropicAdaptiveEffort,
@@ -610,7 +611,9 @@ function handleContentBlockStop(
610
611
 
611
612
  /**
612
613
  * Check if the model supports prompt caching.
613
- * Supported: Anthropic model 3.5 Haiku, Anthropic model 3.7 Sonnet, Anthropic model 4.x+ models, Haiku 4.5+
614
+ * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, and every later Claude
615
+ * generation:
616
+ * https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
614
617
  *
615
618
  * For base models and system-defined inference profiles the model ID / ARN
616
619
  * contains the model name, so we can decide locally.
@@ -619,21 +622,18 @@ function handleContentBlockStop(
619
622
  * set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. Amazon Nova models
620
623
  * have automatic caching and don't need explicit cache points.
621
624
  */
622
- function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean {
625
+ export function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean {
626
+ const claudeSupport = supportsBedrockClaudePromptCaching(model.id);
627
+ if (claudeSupport !== undefined) return claudeSupport;
623
628
  if (model.cost.cacheRead || model.cost.cacheWrite) return true;
624
- const id = model.id.toLowerCase();
625
- // Anthropic model 4.x models (opus-4, sonnet-4, haiku-4)
626
- if (id.includes("claude") && (id.includes("-4-") || id.includes("-4."))) return true;
627
- // Anthropic model 3.5 Haiku, Anthropic model 3.7 Sonnet (legacy naming)
628
- if (id.includes("claude-3-7-sonnet") || id.includes("claude-3-5-haiku")) return true;
629
- // Anthropic model Haiku 4.5+ (new naming)
630
- if (id.includes("claude-haiku")) return true;
631
629
  // Application inference profiles don't contain the model name in the ARN.
632
630
  // Allow users to force cache points via environment variable.
633
631
  if (typeof process !== "undefined" && $flag("AWS_BEDROCK_FORCE_CACHE")) return true;
634
632
  return false;
635
633
  }
636
634
 
635
+ export { parseBedrockClaudeGeneration };
636
+
637
637
  /**
638
638
  * Check if the model supports thinking signatures in reasoningContent.
639
639
  * Only Anthropic Anthropic model models support the signature field.
@@ -203,6 +203,7 @@ export type AnthropicClientOptionsArgs = {
203
203
  maxRetryDelayMs?: number;
204
204
  streamFirstEventTimeoutMs?: number;
205
205
  streamIdleTimeoutMs?: number;
206
+ providerSessionId?: string;
206
207
  };
207
208
  export type AnthropicClientOptionsResult = {
208
209
  isOAuthToken: boolean;
@@ -111,6 +111,11 @@ import {
111
111
  hasCopilotVisionInput,
112
112
  resolveGitHubCopilotBaseUrl,
113
113
  } from "./github-copilot-headers";
114
+ import {
115
+ applyOpenCodeGoSessionHeader,
116
+ resolveOpenCodeGoSessionId,
117
+ wrapFetchForOpenCodeGoSession,
118
+ } from "./opencode-go-session";
114
119
  import { hasAdjacentPrivateThinkingBlocks, transformMessages } from "./transform-messages";
115
120
  import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
116
121
 
@@ -1126,6 +1131,7 @@ export type AnthropicClientOptionsArgs = {
1126
1131
  maxRetryDelayMs?: number;
1127
1132
  streamFirstEventTimeoutMs?: number;
1128
1133
  streamIdleTimeoutMs?: number;
1134
+ providerSessionId?: string;
1129
1135
  };
1130
1136
 
1131
1137
  export type AnthropicClientOptionsResult = {
@@ -1940,6 +1946,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1940
1946
  maxRetryDelayMs: options?.maxRetryDelayMs,
1941
1947
  streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
1942
1948
  streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
1949
+ providerSessionId: options?.providerSessionId,
1943
1950
  });
1944
1951
  client = created.client;
1945
1952
  isOAuthToken = created.isOAuthToken;
@@ -3107,7 +3114,11 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
3107
3114
  const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
3108
3115
  const baseFetch = args.fetch ?? fetch;
3109
3116
  const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
3110
- const debugFetch = onSseEvent ? wrapFetchForSseDebug(boundedFetch, event => onSseEvent(event, model)) : boundedFetch;
3117
+ const openCodeGoSessionId = resolveOpenCodeGoSessionId(model, baseUrl, args.providerSessionId, "anthropic");
3118
+ const providerScopedFetch = wrapFetchForOpenCodeGoSession(boundedFetch, openCodeGoSessionId);
3119
+ const debugFetch = onSseEvent
3120
+ ? wrapFetchForSseDebug(providerScopedFetch, event => onSseEvent(event, model))
3121
+ : providerScopedFetch;
3111
3122
  // Bound the connect/headers phase. The first-event watchdog arms only after
3112
3123
  // response headers arrive, so a request whose connection dies before headers
3113
3124
  // was previously governed only by the Anthropic SDK's 10-minute default per
@@ -3124,16 +3135,19 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
3124
3135
  if (needsFineGrainedToolStreamingBeta) {
3125
3136
  betaFeatures.push(fineGrainedToolStreamingBeta);
3126
3137
  }
3127
- const defaultHeaders = mergeHeaders(
3128
- {
3129
- Accept: stream ? "text/event-stream" : "application/json",
3130
- "Anthropic-Dangerous-Direct-Browser-Access": "true",
3131
- Authorization: `Bearer ${copilotApiKey}`,
3132
- ...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
3133
- },
3134
- model.headers,
3135
- dynamicHeaders,
3136
- headers,
3138
+ const defaultHeaders = applyOpenCodeGoSessionHeader(
3139
+ mergeHeaders(
3140
+ {
3141
+ Accept: stream ? "text/event-stream" : "application/json",
3142
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
3143
+ Authorization: `Bearer ${copilotApiKey}`,
3144
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
3145
+ },
3146
+ model.headers,
3147
+ dynamicHeaders,
3148
+ headers,
3149
+ ),
3150
+ openCodeGoSessionId,
3137
3151
  );
3138
3152
 
3139
3153
  return {
@@ -3159,16 +3173,19 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
3159
3173
  betaFeatures.push(interleavedThinkingBeta);
3160
3174
  }
3161
3175
 
3162
- const defaultHeaders = buildAnthropicHeaders({
3163
- apiKey,
3164
- baseUrl,
3165
- isOAuth: oauthToken,
3166
- extraBetas: betaFeatures,
3167
- stream,
3168
- modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
3169
- isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
3170
- zcodeSourceHeaders: model.provider === "glm-zcode",
3171
- });
3176
+ const defaultHeaders = applyOpenCodeGoSessionHeader(
3177
+ buildAnthropicHeaders({
3178
+ apiKey,
3179
+ baseUrl,
3180
+ isOAuth: oauthToken,
3181
+ extraBetas: betaFeatures,
3182
+ stream,
3183
+ modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
3184
+ isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
3185
+ zcodeSourceHeaders: model.provider === "glm-zcode",
3186
+ }),
3187
+ openCodeGoSessionId,
3188
+ );
3172
3189
 
3173
3190
  if (model.provider === "cloudflare-ai-gateway") {
3174
3191
  return {
@@ -689,7 +689,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
689
689
  conversationUsageContextCache.set(conversationId, usageContext);
690
690
  const reusableCachedState =
691
691
  cachedState && canReuseCursorUsageContext(previousUsageContext, usageContext) ? cachedState : undefined;
692
- const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
692
+ const { requestBytes, conversationState } = await buildGrpcRequest(model, context, options, {
693
693
  conversationId,
694
694
  blobStore,
695
695
  conversationState: reusableCachedState,
@@ -3459,7 +3459,7 @@ function extractImages(content: (TextContent | ImageContent)[]) {
3459
3459
  );
3460
3460
  }
3461
3461
 
3462
- function buildGrpcRequest(
3462
+ async function buildGrpcRequest(
3463
3463
  model: Model<"cursor-agent">,
3464
3464
  context: Context,
3465
3465
  options: CursorOptions | undefined,
@@ -3468,11 +3468,11 @@ function buildGrpcRequest(
3468
3468
  blobStore: Map<string, Uint8Array>;
3469
3469
  conversationState?: ConversationStateStructure;
3470
3470
  },
3471
- ): {
3471
+ ): Promise<{
3472
3472
  requestBytes: Uint8Array;
3473
3473
  blobStore: Map<string, Uint8Array>;
3474
3474
  conversationState: ConversationStateStructure;
3475
- } {
3475
+ }> {
3476
3476
  const blobStore = state.blobStore;
3477
3477
 
3478
3478
  const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt, model.id).map(json =>
@@ -3558,7 +3558,7 @@ function buildGrpcRequest(
3558
3558
  displayName: model.name,
3559
3559
  });
3560
3560
 
3561
- const runRequest = create(AgentRunRequestSchema, {
3561
+ let runRequest = create(AgentRunRequestSchema, {
3562
3562
  conversationState,
3563
3563
  action,
3564
3564
  modelDetails,
@@ -3573,7 +3573,13 @@ function buildGrpcRequest(
3573
3573
  : {}),
3574
3574
  });
3575
3575
 
3576
- options?.onPayload?.(runRequest, model, options?.attemptScope);
3576
+ if (options?.onPayload) {
3577
+ const payload = toJson(AgentRunRequestSchema, runRequest);
3578
+ const replacement = await options.onPayload(payload, model, options.attemptScope);
3579
+ if (replacement !== undefined) {
3580
+ runRequest = fromJson(AgentRunRequestSchema, replacement as JsonValue);
3581
+ }
3582
+ }
3577
3583
 
3578
3584
  // Tools are sent later via requestContext (exec handshake)
3579
3585
 
@@ -25,7 +25,7 @@ const REQUEST_BLOCKED_MESSAGE_RE = /^\s*request blocked\b/i;
25
25
 
26
26
  export async function parseCodexError(response: Response): Promise<CodexErrorInfo> {
27
27
  const raw = await response.text();
28
- let message = raw || response.statusText || "Request failed";
28
+ let message = raw || response.statusText || `Codex request failed (HTTP ${response.status})`;
29
29
  let friendlyMessage: string | undefined;
30
30
  let rateLimits: CodexRateLimits | undefined;
31
31
  let code: string | undefined;
@@ -51,6 +51,10 @@ import {
51
51
  normalizeSystemPrompts,
52
52
  sanitizeOpenAIResponsesHistoryItemsForReplay,
53
53
  } from "../utils";
54
+ import {
55
+ formatOpenAICodexChatGPTEntitlementError,
56
+ isOpenAICodexChatGPTEntitlementError,
57
+ } from "../utils/codex-entitlement";
54
58
  import { AssistantMessageEventStream } from "../utils/event-stream";
55
59
  import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE, transportFailureFacts } from "../utils/fallback-transport";
56
60
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
@@ -1234,7 +1238,7 @@ function handleCodexStreamEvent(args: {
1234
1238
  }
1235
1239
 
1236
1240
  if (eventType === "error" || eventType === "response.failed") {
1237
- throw createCodexProviderStreamError(rawEvent);
1241
+ throw createCodexProviderStreamError(rawEvent, model.id);
1238
1242
  }
1239
1243
 
1240
1244
  return firstTokenTime;
@@ -2853,7 +2857,11 @@ async function openCodexSseEventStream(
2853
2857
  updateCodexSessionMetadataFromHeaders(state, response.headers);
2854
2858
  if (!response.ok) {
2855
2859
  const info = await parseCodexError(response);
2856
- const error = new Error(info.friendlyMessage || info.message);
2860
+ const error = new Error(
2861
+ isOpenAICodexChatGPTEntitlementError(info.message, info.code)
2862
+ ? formatOpenAICodexChatGPTEntitlementError(body.model)
2863
+ : info.friendlyMessage || info.message,
2864
+ );
2857
2865
  (error as { headers?: Headers; status?: number }).headers = response.headers;
2858
2866
  (error as { headers?: Headers; status?: number }).status = response.status;
2859
2867
  (error as { code?: string }).code = info.code;
@@ -3235,11 +3243,12 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
3235
3243
  return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
3236
3244
  }
3237
3245
 
3238
- function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
3246
+ function createCodexProviderStreamError(rawEvent: Record<string, unknown>, modelId: string): CodexProviderStreamError {
3239
3247
  const code = getCodexEventErrorCode(rawEvent);
3240
3248
  const message = getCodexEventErrorMessage(rawEvent);
3241
- const formattedMessage =
3242
- typeof rawEvent.type === "string" && rawEvent.type === "error"
3249
+ const formattedMessage = isOpenAICodexChatGPTEntitlementError(message, code)
3250
+ ? formatOpenAICodexChatGPTEntitlementError(modelId)
3251
+ : typeof rawEvent.type === "string" && rawEvent.type === "error"
3243
3252
  ? formatCodexErrorEvent(rawEvent, code, message)
3244
3253
  : (formatCodexFailure(rawEvent) ?? "Codex response failed");
3245
3254
  return new CodexProviderStreamError(
@@ -3,4 +3,4 @@
3
3
  * the core-safe module so model metadata can use it without loading provider
4
4
  * implementations during startup.
5
5
  */
6
- export { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "../openai-completions-compat";
6
+ export { detectOpenAICompat, type GrokGeneration, isDirectXaiReasoningEffortModel, parseDirectXaiReasoningEffortGeneration, type ResolvedOpenAICompat, resolveOpenAICompat, } from "../openai-completions-compat";
@@ -3,4 +3,11 @@
3
3
  * the core-safe module so model metadata can use it without loading provider
4
4
  * implementations during startup.
5
5
  */
6
- export { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "../openai-completions-compat";
6
+ export {
7
+ detectOpenAICompat,
8
+ type GrokGeneration,
9
+ isDirectXaiReasoningEffortModel,
10
+ parseDirectXaiReasoningEffortGeneration,
11
+ type ResolvedOpenAICompat,
12
+ resolveOpenAICompat,
13
+ } from "../openai-completions-compat";
@@ -100,6 +100,11 @@ import {
100
100
  wrapFetchForOpenAIRequestTransform,
101
101
  } from "./openai-request-transform";
102
102
  import { createInitialResponsesAssistantMessage } from "./openai-responses-shared";
103
+ import {
104
+ applyOpenCodeGoSessionHeader,
105
+ resolveOpenCodeGoSessionId,
106
+ wrapFetchForOpenCodeGoSession,
107
+ } from "./opencode-go-session";
103
108
  import { transformMessages } from "./transform-messages";
104
109
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
105
110
 
@@ -602,6 +607,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
602
607
  options?.authCredentialType,
603
608
  options?.requestMaxRetries,
604
609
  options?.sessionId,
610
+ options?.providerSessionId,
605
611
  options?.maxRetryDelayMs,
606
612
  options?.attemptScope,
607
613
  );
@@ -1259,6 +1265,7 @@ async function createClient(
1259
1265
  authCredentialType?: OpenAICompletionsOptions["authCredentialType"],
1260
1266
  requestMaxRetries?: number,
1261
1267
  sessionId?: string,
1268
+ providerSessionId?: string,
1262
1269
  maxRetryDelayMs?: number,
1263
1270
  attemptScope?: import("../types.js").AttemptScopeRef,
1264
1271
  ): Promise<{
@@ -1356,6 +1363,11 @@ async function createClient(
1356
1363
  const endpointRequestQuery = endpointQuery;
1357
1364
  const requestQuery =
1358
1365
  [endpointRequestQuery, azureQuery].filter((query): query is string => query !== undefined).join("&") || undefined;
1366
+ const openCodeGoSessionId = resolveOpenCodeGoSessionId(model, baseUrl, providerSessionId, "openai");
1367
+ // Reserve the provider-specific header on every OpenAI-compatible route. Caller,
1368
+ // model, and transform values are removed unless the exact OpenCode Go endpoint
1369
+ // has an opaque identity owned by the agent's conversation lifecycle.
1370
+ headers = applyOpenCodeGoSessionHeader(headers, openCodeGoSessionId);
1359
1371
  let capturedErrorResponse: CapturedHttpErrorResponse | undefined;
1360
1372
  const baseFetch = fetchOverride ?? fetch;
1361
1373
  const wrappedFetch = Object.assign(
@@ -1389,8 +1401,9 @@ async function createClient(
1389
1401
  baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
1390
1402
  );
1391
1403
  const boundedFetch = wrapOpenAIFetchForBoundedRateLimits(wrappedFetch, maxRetryDelayMs);
1404
+ const providerScopedFetch = wrapFetchForOpenCodeGoSession(boundedFetch, openCodeGoSessionId);
1392
1405
  const transformedFetch = wrapFetchForOpenAIRequestTransform(
1393
- boundedFetch,
1406
+ providerScopedFetch,
1394
1407
  model.requestTransform,
1395
1408
  `Gajae-Code/${packageJson.version}`,
1396
1409
  );
@@ -93,6 +93,11 @@ import {
93
93
  processResponsesStream,
94
94
  repairOrphanResponsesToolOutputs,
95
95
  } from "./openai-responses-shared";
96
+ import {
97
+ applyOpenCodeGoSessionHeader,
98
+ resolveOpenCodeGoSessionId,
99
+ wrapFetchForOpenCodeGoSession,
100
+ } from "./opencode-go-session";
96
101
  import { transformMessages } from "./transform-messages";
97
102
 
98
103
  /**
@@ -378,6 +383,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
378
383
  options?.headers,
379
384
  options?.initiatorOverride,
380
385
  cacheSessionId,
386
+ options?.providerSessionId,
381
387
  cacheRetention,
382
388
  options?.onSseEvent,
383
389
  options?.fetch,
@@ -556,6 +562,7 @@ function createClient(
556
562
  extraHeaders?: Record<string, string>,
557
563
  initiatorOverride?: MessageAttribution,
558
564
  sessionId?: string,
565
+ providerSessionId?: string,
559
566
  cacheRetention?: CacheRetention,
560
567
  onSseEvent?: OpenAIResponsesOptions["onSseEvent"],
561
568
  fetchOverride?: FetchImpl,
@@ -618,6 +625,8 @@ function createClient(
618
625
  }
619
626
  headers = applyOpenAIRequestTransformHeaders(headers, model.requestTransform, `Gajae-Code/${packageJson.version}`);
620
627
  const { baseUrl: clientBaseUrl, query: endpointQuery } = splitBaseUrlQuery(baseUrl);
628
+ const openCodeGoSessionId = resolveOpenCodeGoSessionId(model, baseUrl, providerSessionId, "openai");
629
+ headers = applyOpenCodeGoSessionHeader(headers, openCodeGoSessionId);
621
630
  const baseFetch = fetchOverride ?? fetch;
622
631
  const queryFetch = Object.assign(
623
632
  async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
@@ -626,8 +635,9 @@ function createClient(
626
635
  baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
627
636
  );
628
637
  const boundedFetch = wrapOpenAIFetchForBoundedRateLimits(queryFetch, maxRetryDelayMs);
638
+ const providerScopedFetch = wrapFetchForOpenCodeGoSession(boundedFetch, openCodeGoSessionId);
629
639
  const transformedFetch = wrapFetchForOpenAIRequestTransform(
630
- boundedFetch,
640
+ providerScopedFetch,
631
641
  model.requestTransform,
632
642
  `Gajae-Code/${packageJson.version}`,
633
643
  );
@@ -0,0 +1,5 @@
1
+ import type { FetchImpl, Model } from "../types";
2
+ export type OpenCodeGoApiFamily = "openai" | "anthropic";
3
+ export declare function resolveOpenCodeGoSessionId(model: Pick<Model, "provider">, baseUrl: string | undefined, providerSessionId: string | undefined, apiFamily: OpenCodeGoApiFamily): string | undefined;
4
+ export declare function applyOpenCodeGoSessionHeader(headers: Record<string, string>, sessionId: string | undefined): Record<string, string>;
5
+ export declare function wrapFetchForOpenCodeGoSession(baseFetch: FetchImpl, sessionId: string | undefined): FetchImpl;
@@ -0,0 +1,57 @@
1
+ import type { FetchImpl, Model } from "../types";
2
+
3
+ const OPENCODE_GO_ORIGIN = "https://opencode.ai";
4
+ const OPENCODE_GO_OPENAI_BASE_PATH = "/zen/go/v1";
5
+ const OPENCODE_GO_ANTHROPIC_BASE_PATH = "/zen/go";
6
+
7
+ export type OpenCodeGoApiFamily = "openai" | "anthropic";
8
+
9
+ function expectedBasePath(apiFamily: OpenCodeGoApiFamily): string {
10
+ return apiFamily === "openai" ? OPENCODE_GO_OPENAI_BASE_PATH : OPENCODE_GO_ANTHROPIC_BASE_PATH;
11
+ }
12
+
13
+ export function resolveOpenCodeGoSessionId(
14
+ model: Pick<Model, "provider">,
15
+ baseUrl: string | undefined,
16
+ providerSessionId: string | undefined,
17
+ apiFamily: OpenCodeGoApiFamily,
18
+ ): string | undefined {
19
+ if (model.provider !== "opencode-go" || !baseUrl || !providerSessionId) return undefined;
20
+ try {
21
+ const url = new URL(baseUrl);
22
+ if (url.origin !== OPENCODE_GO_ORIGIN) return undefined;
23
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return undefined;
24
+ if (url.pathname.replace(/\/+$/u, "") !== expectedBasePath(apiFamily)) return undefined;
25
+ return providerSessionId;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ export function applyOpenCodeGoSessionHeader(
32
+ headers: Record<string, string>,
33
+ sessionId: string | undefined,
34
+ ): Record<string, string> {
35
+ const normalizedHeaders = new Headers(headers);
36
+ normalizedHeaders.delete("x-opencode-session");
37
+ if (sessionId) normalizedHeaders.set("x-opencode-session", sessionId);
38
+ return Object.fromEntries(normalizedHeaders.entries());
39
+ }
40
+
41
+ export function wrapFetchForOpenCodeGoSession(baseFetch: FetchImpl, sessionId: string | undefined): FetchImpl {
42
+ return Object.assign(
43
+ async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
44
+ if (input instanceof Request) {
45
+ const request = new Request(input, init);
46
+ request.headers.delete("x-opencode-session");
47
+ if (sessionId) request.headers.set("x-opencode-session", sessionId);
48
+ return baseFetch(request);
49
+ }
50
+ const headers = new Headers(init?.headers);
51
+ headers.delete("x-opencode-session");
52
+ if (sessionId) headers.set("x-opencode-session", sessionId);
53
+ return baseFetch(input, { ...init, headers });
54
+ },
55
+ baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
56
+ );
57
+ }
package/src/stream.ts CHANGED
@@ -955,6 +955,7 @@ function mapOptionsForApi<TApi extends Api>(
955
955
  streamMaxRetries: options?.fallbackManaged ? 0 : options?.streamMaxRetries,
956
956
  metadata: options?.metadata,
957
957
  sessionId: options?.sessionId,
958
+ providerSessionId: options?.providerSessionId,
958
959
  providerSessionState: options?.providerSessionState,
959
960
  onPayload: options?.onPayload,
960
961
  onResponse: options?.onResponse,
package/src/types.d.ts CHANGED
@@ -239,6 +239,13 @@ export interface StreamOptions {
239
239
  * session-aware features. Ignored by providers that don't support it.
240
240
  */
241
241
  sessionId?: string;
242
+ /**
243
+ * Opaque conversation identity owned by the calling agent/session lifecycle.
244
+ * Unlike `sessionId`, this MUST NOT be synthesized from prompts, credentials,
245
+ * paths, cache keys, or other request content. Providers with a dedicated
246
+ * conversation header may use this only when their endpoint policy permits it.
247
+ */
248
+ providerSessionId?: string;
242
249
  /**
243
250
  * Provider-scoped mutable state store for this agent session.
244
251
  * Providers can use this to persist transport/session state between turns.
package/src/types.ts CHANGED
@@ -443,6 +443,13 @@ export interface StreamOptions {
443
443
  * session-aware features. Ignored by providers that don't support it.
444
444
  */
445
445
  sessionId?: string;
446
+ /**
447
+ * Opaque conversation identity owned by the calling agent/session lifecycle.
448
+ * Unlike `sessionId`, this MUST NOT be synthesized from prompts, credentials,
449
+ * paths, cache keys, or other request content. Providers with a dedicated
450
+ * conversation header may use this only when their endpoint policy permits it.
451
+ */
452
+ providerSessionId?: string;
446
453
  /**
447
454
  * Provider-scoped mutable state store for this agent session.
448
455
  * Providers can use this to persist transport/session state between turns.