@gajae-code/ai 0.7.3 → 0.7.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,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.5] - 2026-06-27
6
+
7
+ ### Fixed
8
+
9
+ - Sanitized Codex history replay text fields so malformed/non-string replay text no longer corrupts the rebuilt request payload (#1199).
10
+ - Preserved object-valued tool replay fields when rebuilding Codex history payloads instead of coercing them to strings (#1200).
11
+
12
+ ## [0.7.4] - 2026-06-27
13
+
14
+ ### Fixed
15
+
16
+ - Treated `openai-codex-device` as an auth-storage alias for `openai-codex`, so headless/device Codex logins show as authenticated and logout/remove the stored Codex credential instead of appearing unsaved (#1151).
17
+ - Disabled thinking for OpenCode Go forced tool calls so forced-tool turns no longer emit unsupported thinking content (#1185).
18
+ - Restored the GPT-5.5 context window to its correct size (#1186).
19
+
5
20
  ## [0.7.3] - 2026-06-25
6
21
  ### Added
7
22
 
@@ -41,7 +41,7 @@ export declare function applyGeneratedModelPolicies(models: ApiModel<Api>[]): vo
41
41
  * model with a larger context window on the same provider:
42
42
  * - `OpenAI code backend-spark` variants promote to `gpt-5.5`.
43
43
  *
44
- * `gpt-5.5` itself is a 400K-context model and is not demoted to `gpt-5.4`
44
+ * `gpt-5.5` itself is a 1M-context model and is not demoted to `gpt-5.4`
45
45
  * (which has a smaller window), so it has no promotion target.
46
46
  */
47
47
  export declare function linkOpenAIPromotionTargets(models: ApiModel<Api>[]): void;
@@ -32,6 +32,7 @@ export declare function getOAuthApiKey(provider: OAuthProvider, credentials: Rec
32
32
  newCredentials: OAuthCredentials;
33
33
  apiKey: string;
34
34
  } | null>;
35
+ export declare function resolveOAuthStorageProvider(provider: OAuthProviderId): OAuthProviderId;
35
36
  /**
36
37
  * Get list of OAuth providers.
37
38
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.7.3",
4
+ "version": "0.7.5",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.7.3",
46
+ "@gajae-code/utils": "0.7.5",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -31,7 +31,7 @@ import { grokCliRankingStrategy, grokCliUsageProvider } from "./usage/grok-cli";
31
31
  import { kimiUsageProvider } from "./usage/kimi";
32
32
  import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
33
33
  import { zaiUsageProvider } from "./usage/zai";
34
- import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./utils/oauth";
34
+ import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken, resolveOAuthStorageProvider } from "./utils/oauth";
35
35
  import { loginDeepSeek } from "./utils/oauth/deepseek";
36
36
  import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
37
37
  import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
@@ -918,7 +918,8 @@ export class AuthStorage {
918
918
  * @returns Array of stored credentials, empty if none exist
919
919
  */
920
920
  #getStoredCredentials(provider: string): StoredCredential[] {
921
- return this.#data.get(provider) ?? [];
921
+ const storageProvider = resolveOAuthStorageProvider(provider);
922
+ return this.#data.get(storageProvider) ?? [];
922
923
  }
923
924
 
924
925
  /**
@@ -1250,16 +1251,17 @@ export class AuthStorage {
1250
1251
  * Set credential for a provider.
1251
1252
  */
1252
1253
  async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
1254
+ const storageProvider = resolveOAuthStorageProvider(provider);
1253
1255
  const normalized = Array.isArray(credential) ? credential : [credential];
1254
- const deduped = this.#dedupeOAuthCredentials(provider, normalized);
1256
+ const deduped = this.#dedupeOAuthCredentials(storageProvider, normalized);
1255
1257
  const stored = this.#store.replaceAuthCredentialsRemote
1256
- ? await this.#store.replaceAuthCredentialsRemote(provider, deduped)
1257
- : this.#store.replaceAuthCredentialsForProvider(provider, deduped);
1258
+ ? await this.#store.replaceAuthCredentialsRemote(storageProvider, deduped)
1259
+ : this.#store.replaceAuthCredentialsForProvider(storageProvider, deduped);
1258
1260
  this.#setStoredCredentials(
1259
- provider,
1261
+ storageProvider,
1260
1262
  stored.map(record => ({ id: record.id, credential: record.credential })),
1261
1263
  );
1262
- this.#resetProviderAssignments(provider);
1264
+ this.#resetProviderAssignments(storageProvider);
1263
1265
  }
1264
1266
 
1265
1267
  #toSnapshotEntries(provider: string, stored: StoredAuthCredential[]): AuthCredentialSnapshotEntry[] {
@@ -1289,26 +1291,30 @@ export class AuthStorage {
1289
1291
  provider: string,
1290
1292
  credential: AuthCredential,
1291
1293
  ): Promise<AuthCredentialIfAbsentSnapshotResult> {
1292
- if (this.#runtimeOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-runtime");
1293
- if (this.#configOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-config");
1294
- if (this.#getCredentialsForProvider(provider).length > 0)
1295
- return this.#snapshotSkipResult(provider, "skipped-existing");
1296
- if (getEnvApiKey(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-env");
1297
- if (this.#fallbackResolver?.(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-fallback");
1294
+ const storageProvider = resolveOAuthStorageProvider(provider);
1295
+ if (this.#runtimeOverrides.has(storageProvider))
1296
+ return this.#snapshotSkipResult(storageProvider, "skipped-existing-runtime");
1297
+ if (this.#configOverrides.has(storageProvider))
1298
+ return this.#snapshotSkipResult(storageProvider, "skipped-existing-config");
1299
+ if (this.#getCredentialsForProvider(storageProvider).length > 0)
1300
+ return this.#snapshotSkipResult(storageProvider, "skipped-existing");
1301
+ if (getEnvApiKey(storageProvider)) return this.#snapshotSkipResult(storageProvider, "skipped-existing-env");
1302
+ if (this.#fallbackResolver?.(storageProvider))
1303
+ return this.#snapshotSkipResult(storageProvider, "skipped-existing-fallback");
1298
1304
 
1299
1305
  const result = this.#store.upsertAuthCredentialRemoteIfAbsent
1300
- ? await this.#store.upsertAuthCredentialRemoteIfAbsent(provider, credential)
1301
- : this.#store.upsertAuthCredentialForProviderIfAbsent(provider, credential);
1306
+ ? await this.#store.upsertAuthCredentialRemoteIfAbsent(storageProvider, credential)
1307
+ : this.#store.upsertAuthCredentialForProviderIfAbsent(storageProvider, credential);
1302
1308
  this.#setStoredCredentials(
1303
- provider,
1309
+ storageProvider,
1304
1310
  result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
1305
1311
  );
1306
- this.#resetProviderAssignments(provider);
1312
+ this.#resetProviderAssignments(storageProvider);
1307
1313
  return {
1308
1314
  inserted: result.inserted,
1309
1315
  reason: result.reason,
1310
1316
  provider: result.provider,
1311
- entries: this.#toSnapshotEntries(provider, result.entries),
1317
+ entries: this.#toSnapshotEntries(storageProvider, result.entries),
1312
1318
  };
1313
1319
  }
1314
1320
 
@@ -1327,13 +1333,14 @@ export class AuthStorage {
1327
1333
  * Remove credential for a provider.
1328
1334
  */
1329
1335
  async remove(provider: string): Promise<void> {
1336
+ const storageProvider = resolveOAuthStorageProvider(provider);
1330
1337
  if (this.#store.deleteAuthCredentialsRemote) {
1331
- await this.#store.deleteAuthCredentialsRemote(provider, "deleted by user");
1338
+ await this.#store.deleteAuthCredentialsRemote(storageProvider, "deleted by user");
1332
1339
  } else {
1333
- this.#store.deleteAuthCredentialsForProvider(provider, "deleted by user");
1340
+ this.#store.deleteAuthCredentialsForProvider(storageProvider, "deleted by user");
1334
1341
  }
1335
- this.#setStoredCredentials(provider, []);
1336
- this.#resetProviderAssignments(provider);
1342
+ this.#setStoredCredentials(storageProvider, []);
1343
+ this.#resetProviderAssignments(storageProvider);
1337
1344
  }
1338
1345
 
1339
1346
  /**
@@ -1355,11 +1362,12 @@ export class AuthStorage {
1355
1362
  * Unlike getApiKey(), this doesn't refresh OAuth tokens.
1356
1363
  */
1357
1364
  hasAuth(provider: string): boolean {
1358
- if (this.#runtimeOverrides.has(provider)) return true;
1359
- if (this.#configOverrides.has(provider)) return true;
1360
- if (this.#getCredentialsForProvider(provider).length > 0) return true;
1361
- if (getEnvApiKey(provider)) return true;
1362
- if (this.#fallbackResolver?.(provider)) return true;
1365
+ const storageProvider = resolveOAuthStorageProvider(provider);
1366
+ if (this.#runtimeOverrides.has(storageProvider)) return true;
1367
+ if (this.#configOverrides.has(storageProvider)) return true;
1368
+ if (this.#getCredentialsForProvider(storageProvider).length > 0) return true;
1369
+ if (getEnvApiKey(storageProvider)) return true;
1370
+ if (this.#fallbackResolver?.(storageProvider)) return true;
1363
1371
  return false;
1364
1372
  }
1365
1373
 
@@ -197,7 +197,7 @@ export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
197
197
  * model with a larger context window on the same provider:
198
198
  * - `OpenAI code backend-spark` variants promote to `gpt-5.5`.
199
199
  *
200
- * `gpt-5.5` itself is a 400K-context model and is not demoted to `gpt-5.4`
200
+ * `gpt-5.5` itself is a 1M-context model and is not demoted to `gpt-5.4`
201
201
  * (which has a smaller window), so it has no promotion target.
202
202
  */
203
203
  export function linkOpenAIPromotionTargets(models: ApiModel<Api>[]): void {
@@ -453,15 +453,8 @@ function inferGeneratedApplyPatchToolType(
453
453
  }
454
454
 
455
455
  function applyGpt55ContextWindow(model: ApiModel<Api>, parsedModel: OpenAIModel): boolean {
456
- // OpenAI Codex reports GPT-5.5 with a 272K prompt budget. Keep the generated
457
- // bundle aligned with the backend limit so compaction fires before the prompt
458
- // crosses the usable Codex window instead of trusting stale 400K snapshots.
459
- if (model.provider === "openai-codex" && parsedModel.variant === "base" && semverEqual(parsedModel.version, "5.5")) {
460
- model.contextWindow = 272000;
461
- return true;
462
- }
463
456
  if (parsedModel.variant === "base" && semverEqual(parsedModel.version, "5.5")) {
464
- model.contextWindow = 400000;
457
+ model.contextWindow = 1_000_000;
465
458
  return true;
466
459
  }
467
460
  return false;
package/src/models.json CHANGED
@@ -56310,7 +56310,7 @@
56310
56310
  "cacheRead": 0.5,
56311
56311
  "cacheWrite": 0
56312
56312
  },
56313
- "contextWindow": 272000,
56313
+ "contextWindow": 1000000,
56314
56314
  "maxTokens": 128000,
56315
56315
  "preferWebsockets": true,
56316
56316
  "priority": 9,
@@ -44,6 +44,7 @@ import {
44
44
  getOpenAIResponsesHistoryItems,
45
45
  getOpenAIResponsesHistoryPayload,
46
46
  normalizeSystemPrompts,
47
+ sanitizeOpenAIResponsesHistoryItemsForReplay,
47
48
  } from "../utils";
48
49
  import { AssistantMessageEventStream } from "../utils/event-stream";
49
50
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
@@ -2550,17 +2551,16 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2550
2551
  for (const msg of transformedMessages) {
2551
2552
  if (msg.role === "user" || msg.role === "developer") {
2552
2553
  const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
2553
- const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
2554
- | Array<ResponseInput[number]>
2555
- | undefined;
2554
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
2556
2555
  if (historyItems) {
2557
- for (const item of historyItems) {
2556
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems);
2557
+ for (const item of sanitizedHistoryItems) {
2558
2558
  const maybe = item as { type?: string; call_id?: string };
2559
2559
  if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2560
2560
  customCallIds.add(maybe.call_id);
2561
2561
  }
2562
2562
  }
2563
- messages.push(...historyItems);
2563
+ messages.push(...sanitizedHistoryItems);
2564
2564
  msgIndex += 1;
2565
2565
  continue;
2566
2566
  }
@@ -2579,18 +2579,19 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2579
2579
  model.provider,
2580
2580
  assistantMsg.provider,
2581
2581
  );
2582
- const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
2582
+ const historyItems = providerPayload?.items;
2583
2583
  if (historyItems) {
2584
- for (const item of historyItems) {
2584
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems);
2585
+ for (const item of sanitizedHistoryItems) {
2585
2586
  const maybe = item as { type?: string; call_id?: string };
2586
2587
  if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2587
2588
  customCallIds.add(maybe.call_id);
2588
2589
  }
2589
2590
  }
2590
2591
  if (providerPayload?.dt) {
2591
- messages.push(...historyItems);
2592
+ messages.push(...sanitizedHistoryItems);
2592
2593
  } else {
2593
- messages.splice(0, messages.length, ...historyItems);
2594
+ messages.splice(0, messages.length, ...sanitizedHistoryItems);
2594
2595
  // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
2595
2596
  }
2596
2597
  msgIndex += 1;
@@ -103,6 +103,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
103
103
  provider === "opencode-go" ||
104
104
  baseUrl.includes("opencode.ai");
105
105
  const isOpenCodeProvider = provider === "opencode-go" || provider === "opencode-zen";
106
+ const isOpenCodeGoReasoning = provider === "opencode-go" && Boolean(model.reasoning);
106
107
 
107
108
  const useMaxTokens =
108
109
  provider === "mistral" ||
@@ -194,7 +195,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
194
195
  supportsReasoningEffort: !isGrok && !isZai,
195
196
  reasoningEffortMap,
196
197
  supportsUsageInStreaming: !isCerebras,
197
- disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,
198
+ disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel || isOpenCodeGoReasoning,
198
199
  disableReasoningOnToolChoice: isDeepseekFamily && Boolean(model.reasoning) && !isOpenRouter,
199
200
  supportsToolChoice: !isDirectDeepseekReasoning,
200
201
  supportsForcedToolChoice: true,
@@ -472,6 +472,9 @@ export async function getOAuthApiKey(
472
472
  return { newCredentials: creds, apiKey };
473
473
  }
474
474
 
475
+ export function resolveOAuthStorageProvider(provider: OAuthProviderId): OAuthProviderId {
476
+ return provider === "openai-codex-device" ? "openai-codex" : provider;
477
+ }
475
478
  /**
476
479
  * Get list of OAuth providers.
477
480
  */
package/src/utils.ts CHANGED
@@ -91,6 +91,57 @@ export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record
91
91
  return sanitized ? [sanitized] : [];
92
92
  });
93
93
  }
94
+ function stringifyResponsesStringParamForReplay(value: unknown): string {
95
+ if (typeof value === "string") return value.toWellFormed();
96
+ try {
97
+ const encoded = JSON.stringify(value);
98
+ if (typeof encoded === "string") return encoded.toWellFormed();
99
+ } catch {
100
+ // Fall through to String().
101
+ }
102
+ return String(value ?? "").toWellFormed();
103
+ }
104
+
105
+ function normalizeResponsesMessageTextForReplay(value: unknown): string {
106
+ if (typeof value === "string") return value.toWellFormed();
107
+ if (value && typeof value === "object") {
108
+ const nestedText = (value as { text?: unknown }).text;
109
+ if (typeof nestedText === "string") return nestedText.toWellFormed();
110
+ }
111
+ return stringifyResponsesStringParamForReplay(value);
112
+ }
113
+
114
+ function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
115
+ if (typeof content === "string") return content.toWellFormed();
116
+ if (!Array.isArray(content)) return content;
117
+ return content.map(part => {
118
+ if (!part || typeof part !== "object") return part;
119
+ const sanitizedPart = { ...(part as Record<string, unknown>) };
120
+ if ("text" in sanitizedPart) {
121
+ sanitizedPart.text = normalizeResponsesMessageTextForReplay(sanitizedPart.text);
122
+ }
123
+ return sanitizedPart;
124
+ });
125
+ }
126
+
127
+ function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>): void {
128
+ if (item.type === "message") {
129
+ item.content = sanitizeResponsesMessageContentForReplay(item.content);
130
+ }
131
+ if (item.type === "function_call" && "arguments" in item && typeof item.arguments !== "string") {
132
+ item.arguments = stringifyResponsesStringParamForReplay(item.arguments);
133
+ }
134
+ if (item.type === "custom_tool_call" && "input" in item && typeof item.input !== "string") {
135
+ item.input = stringifyResponsesStringParamForReplay(item.input);
136
+ }
137
+ if (
138
+ (item.type === "function_call_output" || item.type === "custom_tool_call_output") &&
139
+ "output" in item &&
140
+ typeof item.output !== "string"
141
+ ) {
142
+ item.output = stringifyResponsesStringParamForReplay(item.output);
143
+ }
144
+ }
94
145
 
95
146
  function sanitizeOpenAIResponsesHistoryItemForReplay(
96
147
  item: Record<string, unknown>,
@@ -105,6 +156,7 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
105
156
  if (typeof item.call_id === "string") {
106
157
  sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
107
158
  }
159
+ sanitizeResponsesStringFieldsForReplay(sanitizedItem);
108
160
 
109
161
  return sanitizedItem as unknown as OpenAIResponsesReplayItem;
110
162
  }