@gajae-code/ai 0.16.4 → 0.16.6

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.6] - 2026-09-07
6
+
7
+ ## [0.16.5] - 2026-09-07
8
+
9
+ - Documented `GJC_OPENAI_CODE_WEBSOCKET_V2` as a switch that enables a websocket v2 path. No code read it under that name, under the legacy `PI_CODEX_WEBSOCKET_V2`, or under the `PI_OPENAI_CODE_WEBSOCKET_V2` the historical entry records; the v2 beta header has been unconditional for websocket transport. The documentation row is removed rather than reintroducing a knob, and the test that claimed to gate on it no longer writes an environment variable nothing reads.
10
+ - Maintenance reasoning now fails closed for Anthropic models routed through an unverified custom endpoint and for raw reasoning-enabled models without thinking metadata. This prevents unsupported thinking controls and avoids a synchronous missing-metadata crash before provider wire transformation.
11
+
5
12
  ## [0.16.4] - 2026-09-05
6
13
 
7
14
  ### Added
@@ -256,12 +256,12 @@ export interface StreamOptions {
256
256
  * Return undefined to keep the payload unchanged.
257
257
  * The `scope` parameter carries the per-attempt identity for execution attribution.
258
258
  */
259
- onPayload?: (payload: unknown, model?: Model<Api>, scope?: AttemptScopeRef) => unknown | undefined | Promise<unknown | undefined>;
259
+ onPayload?: (payload: unknown, model?: Model<Api>, scope?: AttemptScopeRef, signal?: AbortSignal) => unknown | undefined | Promise<unknown | undefined>;
260
260
  /**
261
261
  * Optional callback for provider response metadata after headers are received.
262
262
  * The `scope` parameter carries the per-attempt identity for execution attribution.
263
263
  */
264
- onResponse?: (response: ProviderResponseMetadata, model?: Model<Api>, scope?: AttemptScopeRef) => void | Promise<void>;
264
+ onResponse?: (response: ProviderResponseMetadata, model?: Model<Api>, scope?: AttemptScopeRef, signal?: AbortSignal) => void | Promise<void>;
265
265
  /**
266
266
  * Internal dispatch-admission hook. Providers invoke this immediately before
267
267
  * submitting an outbound request; stream forwarding retains a first-response
@@ -3,4 +3,5 @@ export declare function normalizeProviderResponse(response: Response, requestId?
3
3
  export declare function notifyProviderResponse(options: {
4
4
  onResponse?: StreamOptions["onResponse"];
5
5
  attemptScope?: AttemptScopeRef;
6
+ signal?: AbortSignal;
6
7
  } | undefined, response: Response, model?: Model<Api>, requestId?: string | null, metadata?: Record<string, unknown>): Promise<void>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.16.4",
4
+ "version": "0.16.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/natives": "0.16.4",
44
- "@gajae-code/utils": "0.16.4",
43
+ "@gajae-code/natives": "0.16.6",
44
+ "@gajae-code/utils": "0.16.6",
45
45
  "openai": "^6.36.0",
46
46
  "partial-json": "^0.1.7",
47
47
  "zod": "4.4.3"
@@ -242,7 +242,6 @@ export function modelSupportsReasoningControl<TApi extends Api>(
242
242
  resolvedBaseUrl?: string,
243
243
  ): boolean {
244
244
  if (!model.reasoning) return false;
245
-
246
245
  if (model.api === "openai-completions") {
247
246
  const completionsModel = model as ApiModel<"openai-completions">;
248
247
  const explicitSupport = completionsModel.compat?.supportsReasoningEffort;
@@ -366,6 +365,18 @@ export function clampThinkingLevelForModel<TApi extends Api>(
366
365
  if (!modelSupportsReasoningControl(model) || requested === undefined) {
367
366
  return undefined;
368
367
  }
368
+ if (model.api === "anthropic-messages" && model.provider === "anthropic") {
369
+ const baseUrl = model.baseUrl;
370
+ try {
371
+ const url = new URL(baseUrl || "https://api.anthropic.com");
372
+ if (url.protocol !== "https:" || url.hostname !== "api.anthropic.com") return undefined;
373
+ } catch {
374
+ return undefined;
375
+ }
376
+ }
377
+ if (!model.thinking) {
378
+ return undefined;
379
+ }
369
380
 
370
381
  const levels = getSupportedEfforts(model);
371
382
  if (levels.includes(requested)) {
@@ -220,7 +220,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
220
220
  if (tc.any || tc.tool) additionalModelRequestFields = undefined;
221
221
  }
222
222
 
223
- const commandInput: ConverseStreamRequest = {
223
+ let commandInput: ConverseStreamRequest = {
224
224
  messages: convertMessages(context, model, cacheRetention),
225
225
  system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
226
226
  inferenceConfig: {
@@ -231,7 +231,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
231
231
  toolConfig,
232
232
  additionalModelRequestFields,
233
233
  };
234
- options?.onPayload?.(commandInput, model, options?.attemptScope);
234
+ const replacementPayload = await options?.onPayload?.(
235
+ commandInput,
236
+ model,
237
+ options?.attemptScope,
238
+ options?.signal,
239
+ );
240
+ if (replacementPayload !== undefined) {
241
+ commandInput = replacementPayload as typeof commandInput;
242
+ }
235
243
 
236
244
  const host = `bedrock-runtime.${region}.amazonaws.com`;
237
245
  const url = `https://${host}/model/${encodeURIComponent(model.id)}/converse-stream`;
@@ -1999,7 +1999,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1999
1999
  if (dropFastMode) {
2000
2000
  dropAnthropicFastMode(nextParams);
2001
2001
  }
2002
- const replacementPayload = await options?.onPayload?.(nextParams, model, options?.attemptScope);
2002
+ const replacementPayload = await options?.onPayload?.(
2003
+ nextParams,
2004
+ model,
2005
+ options?.attemptScope,
2006
+ options?.signal,
2007
+ );
2003
2008
  if (replacementPayload !== undefined) {
2004
2009
  nextParams = replacementPayload as typeof nextParams;
2005
2010
  }
@@ -129,9 +129,12 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
129
129
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
130
130
  const client = createClient(model, apiKey, options);
131
131
  const { baseUrl } = resolveAzureConfig(model, options);
132
- const params = buildParams(model, context, options, deploymentName, baseUrl);
132
+ let params = buildParams(model, context, options, deploymentName, baseUrl);
133
133
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
134
- options?.onPayload?.(params, model, options?.attemptScope);
134
+ const replacementPayload = await options?.onPayload?.(params, model, options?.attemptScope, options?.signal);
135
+ if (replacementPayload !== undefined) {
136
+ params = replacementPayload as typeof params;
137
+ }
135
138
  rawRequestDump = {
136
139
  provider: model.provider,
137
140
  api: output.api,
@@ -3575,7 +3575,7 @@ async function buildGrpcRequest(
3575
3575
 
3576
3576
  if (options?.onPayload) {
3577
3577
  const payload = toJson(AgentRunRequestSchema, runRequest);
3578
- const replacement = await options.onPayload(payload, model, options.attemptScope);
3578
+ const replacement = await options.onPayload(payload, model, options.attemptScope, options.signal);
3579
3579
  if (replacement !== undefined) {
3580
3580
  runRequest = fromJson(AgentRunRequestSchema, replacement as JsonValue);
3581
3581
  }
@@ -354,7 +354,12 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
354
354
  const endpoints = baseUrl ? [baseUrl] : isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT];
355
355
 
356
356
  let requestBody = buildRequest(model, context, projectId, options, isAntigravity);
357
- const replacementPayload = await options?.onPayload?.(requestBody, model, options?.attemptScope);
357
+ const replacementPayload = await options?.onPayload?.(
358
+ requestBody,
359
+ model,
360
+ options?.attemptScope,
361
+ options?.signal,
362
+ );
358
363
  if (replacementPayload !== undefined) {
359
364
  requestBody = replacementPayload as typeof requestBody;
360
365
  }
@@ -899,7 +899,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
899
899
  try {
900
900
  const plan = await prepare();
901
901
  let params = plan.params;
902
- const replacement = await options?.onPayload?.(params, model, options?.attemptScope);
902
+ const replacement = await options?.onPayload?.(params, model, options?.attemptScope, options?.signal);
903
903
  if (replacement !== undefined) {
904
904
  params = replacement as GenerateContentParameters;
905
905
  }
@@ -611,8 +611,11 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
611
611
  const configuredBaseUrl = model.baseUrl;
612
612
  const usesExplicitBaseUrl = Boolean(configuredBaseUrl) && !isRegionDerivedKiroApiBaseUrl(configuredBaseUrl);
613
613
  const endpoint = configuredBaseUrl || kiroApiBaseUrl(kiroApiRegion(options));
614
- const request = buildApiKeyRequest(model, context, options);
615
- options?.onPayload?.(request, model, options?.attemptScope);
614
+ let request = buildApiKeyRequest(model, context, options);
615
+ const replacementPayload = await options?.onPayload?.(request, model, options?.attemptScope, options?.signal);
616
+ if (replacementPayload !== undefined) {
617
+ request = replacementPayload;
618
+ }
616
619
 
617
620
  const response = await fetch(endpoint, {
618
621
  method: "POST",
@@ -193,11 +193,19 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
193
193
 
194
194
  // Build request
195
195
  const conversationState = buildConversationState(context, model, options);
196
- const requestBody: GenerateAssistantResponseRequest = {
196
+ let requestBody: GenerateAssistantResponseRequest = {
197
197
  conversationState,
198
198
  };
199
199
 
200
- options?.onPayload?.(requestBody, model, options?.attemptScope);
200
+ const replacementPayload = await options?.onPayload?.(
201
+ requestBody,
202
+ model,
203
+ options?.attemptScope,
204
+ options?.signal,
205
+ );
206
+ if (replacementPayload !== undefined) {
207
+ requestBody = replacementPayload as typeof requestBody;
208
+ }
201
209
 
202
210
  const host = `${STREAMING_SERVICE_NAME}.${region}.amazonaws.com`;
203
211
  const url = `https://${host}/`;
@@ -339,6 +339,7 @@ async function runMock(
339
339
  },
340
340
  model,
341
341
  options.attemptScope,
342
+ options.signal,
342
343
  );
343
344
  } catch (err) {
344
345
  stream.fail(err);
@@ -407,7 +407,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
407
407
  const baseUrl = normalizeBaseUrl(model.baseUrl);
408
408
  let body = createChatBody(model, context, options);
409
409
  const sentForcedToolChoice = body.tool_choice === "required";
410
- const replacementPayload = await options.onPayload?.(body, model, options?.attemptScope);
410
+ const replacementPayload = await options.onPayload?.(body, model, options?.attemptScope, options?.signal);
411
411
  if (replacementPayload !== undefined) {
412
412
  body = replacementPayload as typeof body;
413
413
  }
@@ -717,8 +717,16 @@ async function buildCodexRequestContext(
717
717
  const baseUrl = model.baseUrl || CODEX_BASE_URL;
718
718
  const url = resolveCodexResponsesUrl(baseUrl);
719
719
  const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
720
- const transformedBody = await buildTransformedCodexRequestBody(model, context, options);
721
- options?.onPayload?.(transformedBody, model, options?.attemptScope);
720
+ let transformedBody = await buildTransformedCodexRequestBody(model, context, options);
721
+ const replacementPayload = await options?.onPayload?.(
722
+ transformedBody,
723
+ model,
724
+ options?.attemptScope,
725
+ options?.signal,
726
+ );
727
+ if (replacementPayload !== undefined) {
728
+ transformedBody = replacementPayload as typeof transformedBody;
729
+ }
722
730
 
723
731
  const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
724
732
  const rawRequestDump: RawHttpRequestDump = {
@@ -624,15 +624,24 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
624
624
  const createCompletionsStream = async (toolStrictModeOverride?: ToolStrictModeOverride) => {
625
625
  clearCapturedErrorResponse();
626
626
  const effectiveToolStrictModeOverride = disableStrictTools ? "none" : toolStrictModeOverride;
627
- const { params, toolStrictMode } = buildParams(
627
+ const { params: builtParams, toolStrictMode } = buildParams(
628
628
  model,
629
629
  context,
630
630
  options,
631
631
  baseUrl,
632
632
  effectiveToolStrictModeOverride,
633
633
  );
634
+ let params = builtParams;
634
635
  appliedToolStrictMode = toolStrictMode;
635
- options?.onPayload?.(params, undefined, options?.attemptScope);
636
+ const replacementPayload = await options?.onPayload?.(
637
+ params,
638
+ undefined,
639
+ options?.attemptScope,
640
+ options?.signal,
641
+ );
642
+ if (replacementPayload !== undefined) {
643
+ params = replacementPayload as typeof params;
644
+ }
636
645
  rawRequestDump = {
637
646
  provider: model.provider,
638
647
  api: output.api,
@@ -395,9 +395,17 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
395
395
  );
396
396
  const premiumRequestsTotal = copilotPremiumRequests;
397
397
  const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
398
- const { params } = buildParams(model, context, options, providerSessionState, cacheRetention, baseUrl);
398
+ let { params } = buildParams(model, context, options, providerSessionState, cacheRetention, baseUrl);
399
399
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider, model.id);
400
- options?.onPayload?.(params, undefined, options?.attemptScope);
400
+ const replacementPayload = await options?.onPayload?.(
401
+ params,
402
+ undefined,
403
+ options?.attemptScope,
404
+ options?.signal,
405
+ );
406
+ if (replacementPayload !== undefined) {
407
+ params = replacementPayload as typeof params;
408
+ }
401
409
  rawRequestDump = {
402
410
  provider: model.provider,
403
411
  api: output.api,
package/src/types.d.ts CHANGED
@@ -256,12 +256,12 @@ export interface StreamOptions {
256
256
  * Return undefined to keep the payload unchanged.
257
257
  * The `scope` parameter carries the per-attempt identity for execution attribution.
258
258
  */
259
- onPayload?: (payload: unknown, model?: Model<Api>, scope?: AttemptScopeRef) => unknown | undefined | Promise<unknown | undefined>;
259
+ onPayload?: (payload: unknown, model?: Model<Api>, scope?: AttemptScopeRef, signal?: AbortSignal) => unknown | undefined | Promise<unknown | undefined>;
260
260
  /**
261
261
  * Optional callback for provider response metadata after headers are received.
262
262
  * The `scope` parameter carries the per-attempt identity for execution attribution.
263
263
  */
264
- onResponse?: (response: ProviderResponseMetadata, model?: Model<Api>, scope?: AttemptScopeRef) => void | Promise<void>;
264
+ onResponse?: (response: ProviderResponseMetadata, model?: Model<Api>, scope?: AttemptScopeRef, signal?: AbortSignal) => void | Promise<void>;
265
265
  /**
266
266
  * Internal dispatch-admission hook. Providers invoke this immediately before
267
267
  * submitting an outbound request; stream forwarding retains a first-response
package/src/types.ts CHANGED
@@ -464,6 +464,7 @@ export interface StreamOptions {
464
464
  payload: unknown,
465
465
  model?: Model<Api>,
466
466
  scope?: AttemptScopeRef,
467
+ signal?: AbortSignal,
467
468
  ) => unknown | undefined | Promise<unknown | undefined>;
468
469
  /**
469
470
  * Optional callback for provider response metadata after headers are received.
@@ -473,6 +474,7 @@ export interface StreamOptions {
473
474
  response: ProviderResponseMetadata,
474
475
  model?: Model<Api>,
475
476
  scope?: AttemptScopeRef,
477
+ signal?: AbortSignal,
476
478
  ) => void | Promise<void>;
477
479
  /**
478
480
  * Internal dispatch-admission hook. Providers invoke this immediately before
@@ -3,4 +3,5 @@ export declare function normalizeProviderResponse(response: Response, requestId?
3
3
  export declare function notifyProviderResponse(options: {
4
4
  onResponse?: StreamOptions["onResponse"];
5
5
  attemptScope?: AttemptScopeRef;
6
+ signal?: AbortSignal;
6
7
  } | undefined, response: Response, model?: Model<Api>, requestId?: string | null, metadata?: Record<string, unknown>): Promise<void>;
@@ -19,12 +19,19 @@ export function normalizeProviderResponse(
19
19
  }
20
20
 
21
21
  export async function notifyProviderResponse(
22
- options: { onResponse?: StreamOptions["onResponse"]; attemptScope?: AttemptScopeRef } | undefined,
22
+ options:
23
+ | { onResponse?: StreamOptions["onResponse"]; attemptScope?: AttemptScopeRef; signal?: AbortSignal }
24
+ | undefined,
23
25
  response: Response,
24
26
  model?: Model<Api>,
25
27
  requestId?: string | null,
26
28
  metadata?: Record<string, unknown>,
27
29
  ): Promise<void> {
28
30
  if (!options?.onResponse) return;
29
- await options.onResponse(normalizeProviderResponse(response, requestId, metadata), model, options.attemptScope);
31
+ await options.onResponse(
32
+ normalizeProviderResponse(response, requestId, metadata),
33
+ model,
34
+ options.attemptScope,
35
+ options.signal,
36
+ );
30
37
  }