@gajae-code/ai 0.8.0 → 0.8.2
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 +6 -0
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +1 -0
- package/dist/types/auth-storage.d.ts +3 -1
- package/dist/types/utils.d.ts +1 -0
- package/package.json +2 -2
- package/src/auth-broker/remote-store.ts +17 -2
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-storage.ts +74 -7
- package/src/providers/amazon-bedrock.ts +2 -2
- package/src/providers/anthropic-messages-server.ts +2 -1
- package/src/providers/anthropic.ts +2 -3
- package/src/providers/google-shared.ts +3 -3
- package/src/providers/openai-chat-server.ts +3 -2
- package/src/providers/openai-codex/request-transformer.ts +5 -0
- package/src/providers/openai-completions.ts +3 -3
- package/src/providers/openai-responses-server.ts +6 -3
- package/src/providers/openai-responses-shared.ts +3 -3
- package/src/utils.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-07-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Refreshed matching existing OAuth credentials during `importCredentialIfAbsent` and cleared provider usage caches after the write, so external credential import no longer keeps stale tokens or stale usage-limit reports for the same account.
|
|
10
|
+
|
|
5
11
|
## [0.7.9] - 2026-07-01
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -70,6 +70,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
70
70
|
getCache(key: string): string | null;
|
|
71
71
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
72
72
|
cleanExpiredCache(): void;
|
|
73
|
+
deleteCachePrefix(prefix: string): void;
|
|
73
74
|
/**
|
|
74
75
|
* Store-level hook consumed by `AuthStorage` — routes refresh through the
|
|
75
76
|
* broker so the actual refresh token never leaves the broker host. Returns
|
|
@@ -420,6 +420,7 @@ export declare const credentialIfAbsentUploadResponseSchema: z.ZodObject<{
|
|
|
420
420
|
"skipped-existing-fallback": "skipped-existing-fallback";
|
|
421
421
|
"skipped-existing-runtime": "skipped-existing-runtime";
|
|
422
422
|
"skipped-invalid": "skipped-invalid";
|
|
423
|
+
"updated-existing": "updated-existing";
|
|
423
424
|
}>;
|
|
424
425
|
entries: z.ZodArray<z.ZodObject<{
|
|
425
426
|
id: z.ZodNumber;
|
|
@@ -102,7 +102,7 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
102
102
|
credential: SnapshotCredential;
|
|
103
103
|
identityKey: string | null;
|
|
104
104
|
}
|
|
105
|
-
export type AuthCredentialIfAbsentReason = "inserted" | "skipped-existing" | "skipped-existing-runtime" | "skipped-existing-config" | "skipped-existing-env" | "skipped-existing-fallback" | "skipped-invalid";
|
|
105
|
+
export type AuthCredentialIfAbsentReason = "inserted" | "updated-existing" | "skipped-existing" | "skipped-existing-runtime" | "skipped-existing-config" | "skipped-existing-env" | "skipped-existing-fallback" | "skipped-invalid";
|
|
106
106
|
export interface AuthCredentialIfAbsentResult {
|
|
107
107
|
inserted: boolean;
|
|
108
108
|
reason: AuthCredentialIfAbsentReason;
|
|
@@ -147,6 +147,7 @@ export interface AuthCredentialStore {
|
|
|
147
147
|
includeExpired?: boolean;
|
|
148
148
|
}): string | null;
|
|
149
149
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
150
|
+
deleteCachePrefix?(prefix: string): void;
|
|
150
151
|
cleanExpiredCache(): void;
|
|
151
152
|
/**
|
|
152
153
|
* Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
|
|
@@ -646,6 +647,7 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
646
647
|
includeExpired?: boolean;
|
|
647
648
|
}): string | null;
|
|
648
649
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
650
|
+
deleteCachePrefix(prefix: string): void;
|
|
649
651
|
cleanExpiredCache(): void;
|
|
650
652
|
/**
|
|
651
653
|
* Save OAuth credentials for a provider.
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResponseInput } from "openai/resources/responses/responses";
|
|
|
2
2
|
import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types";
|
|
3
3
|
export { isRecord } from "@gajae-code/utils";
|
|
4
4
|
export declare function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[];
|
|
5
|
+
export declare function sanitizeJsonStrings(value: unknown): unknown;
|
|
5
6
|
export declare function toNumber(value: unknown): number | undefined;
|
|
6
7
|
export declare function toPositiveNumber(value: unknown, fallback: number): number;
|
|
7
8
|
export declare function toBoolean(value: unknown): boolean | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -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.8.
|
|
46
|
+
"@gajae-code/utils": "0.8.2",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -86,6 +86,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
86
86
|
#cache: Map<string, CacheEntry> = new Map();
|
|
87
87
|
#usageCache?: UsageCacheEntry;
|
|
88
88
|
#usageInflight?: Promise<UsageReport[] | null>;
|
|
89
|
+
#usageCacheEpoch = 0;
|
|
89
90
|
#closed = false;
|
|
90
91
|
/**
|
|
91
92
|
* `true` once the SSE consumer received its first frame and hasn't dropped
|
|
@@ -465,6 +466,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
465
466
|
}
|
|
466
467
|
}
|
|
467
468
|
|
|
469
|
+
deleteCachePrefix(prefix: string): void {
|
|
470
|
+
for (const key of this.#cache.keys()) {
|
|
471
|
+
if (key.startsWith(prefix)) this.#cache.delete(key);
|
|
472
|
+
}
|
|
473
|
+
if (prefix.startsWith("usage_cache:")) this.#invalidateUsageCache();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#invalidateUsageCache(): void {
|
|
477
|
+
this.#usageCache = undefined;
|
|
478
|
+
this.#usageInflight = undefined;
|
|
479
|
+
this.#usageCacheEpoch += 1;
|
|
480
|
+
}
|
|
481
|
+
|
|
468
482
|
/**
|
|
469
483
|
* Store-level hook consumed by `AuthStorage` — routes refresh through the
|
|
470
484
|
* broker so the actual refresh token never leaves the broker host. Returns
|
|
@@ -561,10 +575,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
561
575
|
return Promise.resolve(cached.reports);
|
|
562
576
|
}
|
|
563
577
|
if (this.#usageInflight) return this.#usageInflight;
|
|
578
|
+
const epoch = this.#usageCacheEpoch;
|
|
564
579
|
const inflight = this.#client
|
|
565
580
|
.fetchUsage()
|
|
566
581
|
.then(body => {
|
|
567
|
-
this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
|
|
582
|
+
if (this.#usageCacheEpoch === epoch) this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
|
|
568
583
|
return body.reports;
|
|
569
584
|
})
|
|
570
585
|
.catch(error => {
|
|
@@ -572,7 +587,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
572
587
|
return null;
|
|
573
588
|
})
|
|
574
589
|
.finally(() => {
|
|
575
|
-
this.#usageInflight = undefined;
|
|
590
|
+
if (this.#usageCacheEpoch === epoch) this.#usageInflight = undefined;
|
|
576
591
|
});
|
|
577
592
|
this.#usageInflight = inflight;
|
|
578
593
|
return inflight;
|
|
@@ -187,6 +187,7 @@ export const credentialDisableResponseSchema = z
|
|
|
187
187
|
// ─── Upload ────────────────────────────────────────────────────────────────
|
|
188
188
|
const credentialIfAbsentReasonSchema = z.enum([
|
|
189
189
|
"inserted",
|
|
190
|
+
"updated-existing",
|
|
190
191
|
"skipped-existing",
|
|
191
192
|
"skipped-existing-runtime",
|
|
192
193
|
"skipped-existing-config",
|
package/src/auth-storage.ts
CHANGED
|
@@ -154,6 +154,7 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
154
154
|
|
|
155
155
|
export type AuthCredentialIfAbsentReason =
|
|
156
156
|
| "inserted"
|
|
157
|
+
| "updated-existing"
|
|
157
158
|
| "skipped-existing"
|
|
158
159
|
| "skipped-existing-runtime"
|
|
159
160
|
| "skipped-existing-config"
|
|
@@ -210,6 +211,7 @@ export interface AuthCredentialStore {
|
|
|
210
211
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
211
212
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
212
213
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
214
|
+
deleteCachePrefix?(prefix: string): void;
|
|
213
215
|
cleanExpiredCache(): void;
|
|
214
216
|
/**
|
|
215
217
|
* Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
|
|
@@ -472,6 +474,7 @@ interface UsageCache {
|
|
|
472
474
|
get<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
473
475
|
getStale<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
474
476
|
set<T>(key: string, entry: UsageCacheEntry<T>): void;
|
|
477
|
+
deletePrefix?(prefix: string): void;
|
|
475
478
|
cleanup?(): void;
|
|
476
479
|
}
|
|
477
480
|
|
|
@@ -662,6 +665,10 @@ class AuthStorageUsageCache implements UsageCache {
|
|
|
662
665
|
this.store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
|
|
663
666
|
}
|
|
664
667
|
|
|
668
|
+
deletePrefix(prefix: string): void {
|
|
669
|
+
this.store.deleteCachePrefix?.(`${USAGE_CACHE_PREFIX}${prefix}`);
|
|
670
|
+
}
|
|
671
|
+
|
|
665
672
|
cleanup(): void {
|
|
666
673
|
this.store.cleanExpiredCache();
|
|
667
674
|
}
|
|
@@ -1297,8 +1304,6 @@ export class AuthStorage {
|
|
|
1297
1304
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-runtime");
|
|
1298
1305
|
if (this.#configOverrides.has(storageProvider))
|
|
1299
1306
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-config");
|
|
1300
|
-
if (this.#getCredentialsForProvider(storageProvider).length > 0)
|
|
1301
|
-
return this.#snapshotSkipResult(storageProvider, "skipped-existing");
|
|
1302
1307
|
if (getEnvApiKey(storageProvider)) return this.#snapshotSkipResult(storageProvider, "skipped-existing-env");
|
|
1303
1308
|
if (this.#fallbackResolver?.(storageProvider))
|
|
1304
1309
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-fallback");
|
|
@@ -1311,6 +1316,7 @@ export class AuthStorage {
|
|
|
1311
1316
|
result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
1312
1317
|
);
|
|
1313
1318
|
this.#resetProviderAssignments(storageProvider);
|
|
1319
|
+
if (result.inserted) this.#invalidateUsageCacheForProvider(storageProvider);
|
|
1314
1320
|
return {
|
|
1315
1321
|
inserted: result.inserted,
|
|
1316
1322
|
reason: result.reason,
|
|
@@ -1328,6 +1334,13 @@ export class AuthStorage {
|
|
|
1328
1334
|
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
1329
1335
|
);
|
|
1330
1336
|
this.#resetProviderAssignments(provider);
|
|
1337
|
+
this.#invalidateUsageCacheForProvider(provider);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
#invalidateUsageCacheForProvider(provider: string): void {
|
|
1341
|
+
this.#usageRequestInFlight.clear();
|
|
1342
|
+
this.#usageReportsInFlight.clear();
|
|
1343
|
+
this.#usageCache.deletePrefix?.(`report:${provider}:`);
|
|
1331
1344
|
}
|
|
1332
1345
|
|
|
1333
1346
|
/**
|
|
@@ -3705,6 +3718,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
3705
3718
|
#getCacheStmt: Statement;
|
|
3706
3719
|
#getCacheIncludingExpiredStmt: Statement;
|
|
3707
3720
|
#upsertCacheStmt: Statement;
|
|
3721
|
+
#deleteCachePrefixStmt: Statement;
|
|
3708
3722
|
#deleteExpiredCacheStmt: Statement;
|
|
3709
3723
|
#closed = false;
|
|
3710
3724
|
|
|
@@ -3744,6 +3758,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
3744
3758
|
this.#upsertCacheStmt = this.#db.prepare(
|
|
3745
3759
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
3746
3760
|
);
|
|
3761
|
+
this.#deleteCachePrefixStmt = this.#db.prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
|
|
3747
3762
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
3748
3763
|
}
|
|
3749
3764
|
|
|
@@ -4102,16 +4117,58 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4102
4117
|
}
|
|
4103
4118
|
|
|
4104
4119
|
const writeIfAbsent = this.#db.transaction(
|
|
4105
|
-
(
|
|
4120
|
+
(
|
|
4121
|
+
providerName: string,
|
|
4122
|
+
item: AuthCredential,
|
|
4123
|
+
record: SerializedCredentialRecord,
|
|
4124
|
+
): AuthCredentialIfAbsentResult => {
|
|
4106
4125
|
const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
|
|
4107
|
-
const existing:
|
|
4126
|
+
const existing: Array<{
|
|
4127
|
+
id: number;
|
|
4128
|
+
credential: AuthCredential;
|
|
4129
|
+
identityKey: string | null;
|
|
4130
|
+
}> = [];
|
|
4108
4131
|
for (const row of existingRows) {
|
|
4109
4132
|
const activeCredential = deserializeCredential(row);
|
|
4110
4133
|
if (!activeCredential) continue;
|
|
4111
|
-
existing.push(
|
|
4134
|
+
existing.push({
|
|
4135
|
+
id: row.id,
|
|
4136
|
+
credential: activeCredential,
|
|
4137
|
+
identityKey: resolveRowCredentialIdentityKey(providerName, row),
|
|
4138
|
+
});
|
|
4112
4139
|
}
|
|
4113
4140
|
if (existing.length > 0) {
|
|
4114
|
-
|
|
4141
|
+
let targetId: number | null = null;
|
|
4142
|
+
for (const row of existing) {
|
|
4143
|
+
if (!matchesReplacementCredential(providerName, row.credential, row.identityKey, item)) continue;
|
|
4144
|
+
if (targetId === null) {
|
|
4145
|
+
targetId = row.id;
|
|
4146
|
+
this.#updateStmt.run(record.credentialType, record.data, record.identityKey, row.id);
|
|
4147
|
+
} else {
|
|
4148
|
+
this.#deleteStmt.run("replaced by newer credential", row.id);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
if (targetId !== null) {
|
|
4153
|
+
return {
|
|
4154
|
+
inserted: true,
|
|
4155
|
+
reason: "updated-existing",
|
|
4156
|
+
provider: providerName,
|
|
4157
|
+
entries: this.listAuthCredentials(providerName),
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
return {
|
|
4162
|
+
inserted: false,
|
|
4163
|
+
reason: "skipped-existing",
|
|
4164
|
+
provider: providerName,
|
|
4165
|
+
entries: existing.map(row => ({
|
|
4166
|
+
id: row.id,
|
|
4167
|
+
provider: providerName,
|
|
4168
|
+
credential: row.credential,
|
|
4169
|
+
disabledCause: null,
|
|
4170
|
+
})),
|
|
4171
|
+
};
|
|
4115
4172
|
}
|
|
4116
4173
|
|
|
4117
4174
|
this.#insertStmt.get(providerName, record.credentialType, record.data, record.identityKey);
|
|
@@ -4124,7 +4181,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4124
4181
|
},
|
|
4125
4182
|
);
|
|
4126
4183
|
|
|
4127
|
-
|
|
4184
|
+
const result = writeIfAbsent.immediate(provider, credential, serialized);
|
|
4185
|
+
if (result.inserted) this.#purgeSupersededDisabledRows(provider, result.entries);
|
|
4186
|
+
return result;
|
|
4128
4187
|
}
|
|
4129
4188
|
|
|
4130
4189
|
/**
|
|
@@ -4221,6 +4280,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4221
4280
|
}
|
|
4222
4281
|
}
|
|
4223
4282
|
|
|
4283
|
+
deleteCachePrefix(prefix: string): void {
|
|
4284
|
+
if (prefix.length === 0) return;
|
|
4285
|
+
try {
|
|
4286
|
+
this.#deleteCachePrefixStmt.run(prefix.length, prefix);
|
|
4287
|
+
} catch {}
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4224
4290
|
cleanExpiredCache(): void {
|
|
4225
4291
|
try {
|
|
4226
4292
|
this.#deleteExpiredCacheStmt.run();
|
|
@@ -4311,6 +4377,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4311
4377
|
this.#getCacheStmt.finalize();
|
|
4312
4378
|
this.#getCacheIncludingExpiredStmt.finalize();
|
|
4313
4379
|
this.#upsertCacheStmt.finalize();
|
|
4380
|
+
this.#deleteCachePrefixStmt.finalize();
|
|
4314
4381
|
this.#deleteExpiredCacheStmt.finalize();
|
|
4315
4382
|
this.#db.close();
|
|
4316
4383
|
}
|
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
ToolChoice,
|
|
29
29
|
ToolResultMessage,
|
|
30
30
|
} from "../types";
|
|
31
|
-
import { normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
31
|
+
import { normalizeToolCallId, resolveCacheRetention, sanitizeJsonStrings } from "../utils";
|
|
32
32
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
33
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
34
34
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
@@ -701,7 +701,7 @@ function convertMessages(
|
|
|
701
701
|
toolUse: {
|
|
702
702
|
toolUseId: normalizeToolCallId(c.id),
|
|
703
703
|
name: c.name,
|
|
704
|
-
input: c.arguments,
|
|
704
|
+
input: sanitizeJsonStrings(c.arguments),
|
|
705
705
|
},
|
|
706
706
|
});
|
|
707
707
|
break;
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ToolResultMessage,
|
|
14
14
|
UserMessage,
|
|
15
15
|
} from "../types";
|
|
16
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
16
17
|
import {
|
|
17
18
|
type AnthropicAssistantContentBlock,
|
|
18
19
|
type AnthropicMessage,
|
|
@@ -433,7 +434,7 @@ function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>
|
|
|
433
434
|
blocks.push({ type: "redacted_thinking", data: c.data });
|
|
434
435
|
break;
|
|
435
436
|
case "toolCall":
|
|
436
|
-
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: c.arguments ?? {} });
|
|
437
|
+
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: sanitizeJsonStrings(c.arguments ?? {}) });
|
|
437
438
|
break;
|
|
438
439
|
}
|
|
439
440
|
}
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
normalizeSystemPrompts,
|
|
54
54
|
normalizeToolCallId,
|
|
55
55
|
resolveCacheRetention,
|
|
56
|
+
sanitizeJsonStrings,
|
|
56
57
|
} from "../utils";
|
|
57
58
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
58
59
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
@@ -2280,8 +2281,6 @@ function buildParams(
|
|
|
2280
2281
|
params.output_config = { effort } as typeof params.output_config;
|
|
2281
2282
|
}
|
|
2282
2283
|
}
|
|
2283
|
-
} else if (options?.thinkingEnabled === false) {
|
|
2284
|
-
params.thinking = { type: "disabled" };
|
|
2285
2284
|
}
|
|
2286
2285
|
}
|
|
2287
2286
|
|
|
@@ -2491,7 +2490,7 @@ export function convertAnthropicMessages(
|
|
|
2491
2490
|
type: "tool_use",
|
|
2492
2491
|
id: block.id,
|
|
2493
2492
|
name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
|
|
2494
|
-
input: block.arguments ?? {},
|
|
2493
|
+
input: sanitizeJsonStrings(block.arguments ?? {}),
|
|
2495
2494
|
});
|
|
2496
2495
|
}
|
|
2497
2496
|
}
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
Tool,
|
|
19
19
|
ToolCall,
|
|
20
20
|
} from "../types";
|
|
21
|
-
import { normalizeSystemPrompts } from "../utils";
|
|
21
|
+
import { normalizeSystemPrompts, sanitizeJsonStrings } from "../utils";
|
|
22
22
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
23
23
|
import { finalizeErrorMessage, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
24
24
|
import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
|
|
@@ -237,7 +237,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
237
237
|
const part: Part = {
|
|
238
238
|
functionCall: {
|
|
239
239
|
name: block.name,
|
|
240
|
-
args: block.arguments ?? {},
|
|
240
|
+
args: sanitizeJsonStrings(block.arguments ?? {}) as Record<string, unknown>,
|
|
241
241
|
...(requiresToolCallId(model.id) ? { id: block.id } : {}),
|
|
242
242
|
},
|
|
243
243
|
};
|
|
@@ -467,7 +467,7 @@ export function pushToolCallEvents(
|
|
|
467
467
|
stream.push({
|
|
468
468
|
type: "toolcall_delta",
|
|
469
469
|
contentIndex,
|
|
470
|
-
delta: JSON.stringify(toolCall.arguments),
|
|
470
|
+
delta: JSON.stringify(sanitizeJsonStrings(toolCall.arguments)),
|
|
471
471
|
partial: output,
|
|
472
472
|
});
|
|
473
473
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ToolResultMessage,
|
|
20
20
|
TSchema,
|
|
21
21
|
} from "../types";
|
|
22
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
22
23
|
import {
|
|
23
24
|
type OpenAIChatContentPart,
|
|
24
25
|
type OpenAIChatMessage,
|
|
@@ -453,9 +454,9 @@ function isOnlyRaw(args: Record<string, unknown>): boolean {
|
|
|
453
454
|
|
|
454
455
|
function stringifyArgs(args: Record<string, unknown>): string {
|
|
455
456
|
// `__raw` is our fallback marker for un-parseable inbound args; preserve it verbatim on the way out.
|
|
456
|
-
if (typeof args.__raw === "string" && isOnlyRaw(args)) return args.__raw;
|
|
457
|
+
if (typeof args.__raw === "string" && isOnlyRaw(args)) return args.__raw.toWellFormed();
|
|
457
458
|
try {
|
|
458
|
-
return JSON.stringify(args);
|
|
459
|
+
return JSON.stringify(sanitizeJsonStrings(args));
|
|
459
460
|
} catch {
|
|
460
461
|
return "{}";
|
|
461
462
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Effort } from "../../model-thinking";
|
|
2
2
|
import { requireSupportedEffort } from "../../model-thinking";
|
|
3
3
|
import type { Api, Model } from "../../types";
|
|
4
|
+
import { sanitizeJsonStrings } from "../../utils";
|
|
4
5
|
|
|
5
6
|
export interface ReasoningConfig {
|
|
6
7
|
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -109,6 +110,10 @@ function normalizeInputTextPartFields(input: InputItem[] | undefined): InputItem
|
|
|
109
110
|
}
|
|
110
111
|
if (normalizedItem.type === "message") {
|
|
111
112
|
normalizedItem.content = normalizeTextPartFields(normalizedItem.content, `input[${itemIndex}].content`);
|
|
113
|
+
} else if (normalizedItem.type === "function_call" && "arguments" in itemRecord) {
|
|
114
|
+
itemRecord.arguments = sanitizeJsonStrings(itemRecord.arguments);
|
|
115
|
+
} else if (normalizedItem.type === "custom_tool_call" && typeof itemRecord.input === "string") {
|
|
116
|
+
itemRecord.input = itemRecord.input.toWellFormed();
|
|
112
117
|
}
|
|
113
118
|
return normalizedItem;
|
|
114
119
|
});
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type ToolChoice,
|
|
36
36
|
type ToolResultMessage,
|
|
37
37
|
} from "../types";
|
|
38
|
-
import { normalizeSystemPrompts } from "../utils";
|
|
38
|
+
import { normalizeSystemPrompts, sanitizeJsonStrings } from "../utils";
|
|
39
39
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
40
40
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
41
41
|
import { toFirepassWireModelId, toFireworksWireModelId } from "../utils/fireworks-model-id";
|
|
@@ -166,7 +166,7 @@ function normalizeStreamingContentText(content: unknown): string {
|
|
|
166
166
|
function serializeToolArguments(value: unknown): string {
|
|
167
167
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
168
168
|
try {
|
|
169
|
-
return JSON.stringify(value);
|
|
169
|
+
return JSON.stringify(sanitizeJsonStrings(value));
|
|
170
170
|
} catch {
|
|
171
171
|
return "{}";
|
|
172
172
|
}
|
|
@@ -178,7 +178,7 @@ function serializeToolArguments(value: unknown): string {
|
|
|
178
178
|
try {
|
|
179
179
|
const parsed = JSON.parse(trimmed);
|
|
180
180
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
181
|
-
return JSON.stringify(parsed);
|
|
181
|
+
return JSON.stringify(sanitizeJsonStrings(parsed));
|
|
182
182
|
}
|
|
183
183
|
} catch {}
|
|
184
184
|
return "{}";
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
Tool,
|
|
23
23
|
ToolCall,
|
|
24
24
|
} from "../types";
|
|
25
|
+
import { sanitizeJsonStrings } from "../utils";
|
|
25
26
|
import {
|
|
26
27
|
type OpenAIResponsesFunctionCallItem,
|
|
27
28
|
type OpenAIResponsesFunctionCallOutputItem,
|
|
@@ -612,7 +613,8 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
|
|
|
612
613
|
} else if (part.type === "toolCall") {
|
|
613
614
|
flushMessage();
|
|
614
615
|
if (part.customWireName) {
|
|
615
|
-
const rawInput =
|
|
616
|
+
const rawInput =
|
|
617
|
+
typeof part.arguments?.input === "string" ? (part.arguments.input as string).toWellFormed() : "";
|
|
616
618
|
out.push({
|
|
617
619
|
type: "custom_tool_call",
|
|
618
620
|
id: part.thoughtSignature ?? makeCustomCallId(),
|
|
@@ -627,7 +629,7 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
|
|
|
627
629
|
id: part.thoughtSignature ?? makeFuncCallId(),
|
|
628
630
|
call_id: part.id,
|
|
629
631
|
name: part.name,
|
|
630
|
-
arguments: JSON.stringify(part.arguments ?? {}),
|
|
632
|
+
arguments: JSON.stringify(sanitizeJsonStrings(part.arguments ?? {})),
|
|
631
633
|
status: "completed",
|
|
632
634
|
});
|
|
633
635
|
}
|
|
@@ -1085,7 +1087,8 @@ export function encodeStream(
|
|
|
1085
1087
|
} else {
|
|
1086
1088
|
// Standard JSON tool: arguments object on the gjc side, the
|
|
1087
1089
|
// wire wants the JSON string the model emitted (= streamed deltas).
|
|
1088
|
-
const argsJson =
|
|
1090
|
+
const argsJson =
|
|
1091
|
+
cur.argsText.toWellFormed() || JSON.stringify(sanitizeJsonStrings(tc.arguments ?? {}));
|
|
1089
1092
|
cur.argsText = argsJson;
|
|
1090
1093
|
emit("response.function_call_arguments.done", {
|
|
1091
1094
|
item_id: cur.itemId,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type ToolCall,
|
|
29
29
|
type ToolResultMessage,
|
|
30
30
|
} from "../types";
|
|
31
|
-
import { normalizeResponsesToolCallId } from "../utils";
|
|
31
|
+
import { normalizeResponsesToolCallId, sanitizeJsonStrings } from "../utils";
|
|
32
32
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
33
|
import { isCompleteJson, parseStreamingJson } from "../utils/json-parse";
|
|
34
34
|
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
|
|
@@ -275,7 +275,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
275
275
|
}
|
|
276
276
|
knownCallIds.add(normalized.callId);
|
|
277
277
|
if (block.customWireName) {
|
|
278
|
-
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
|
|
278
|
+
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input.toWellFormed() : "";
|
|
279
279
|
customCallIds?.add(normalized.callId);
|
|
280
280
|
outputItems.push({
|
|
281
281
|
type: "custom_tool_call",
|
|
@@ -291,7 +291,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
291
291
|
id: itemId,
|
|
292
292
|
call_id: normalized.callId,
|
|
293
293
|
name: block.name,
|
|
294
|
-
arguments: JSON.stringify(block.arguments),
|
|
294
|
+
arguments: JSON.stringify(sanitizeJsonStrings(block.arguments ?? {})),
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
|
package/src/utils.ts
CHANGED
|
@@ -11,6 +11,34 @@ export function normalizeSystemPrompts(systemPrompt: readonly string[] | string
|
|
|
11
11
|
return prompts.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function sanitizeJsonStrings(value: unknown): unknown {
|
|
15
|
+
return sanitizeJsonStringsInner(value, new WeakMap<object, unknown>());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sanitizeJsonStringsInner(value: unknown, seen: WeakMap<object, unknown>): unknown {
|
|
19
|
+
if (typeof value === "string") return value.toWellFormed();
|
|
20
|
+
if (!value || typeof value !== "object") return value;
|
|
21
|
+
|
|
22
|
+
const cached = seen.get(value);
|
|
23
|
+
if (cached !== undefined) return cached;
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
const sanitized: unknown[] = [];
|
|
27
|
+
seen.set(value, sanitized);
|
|
28
|
+
for (const item of value) {
|
|
29
|
+
sanitized.push(sanitizeJsonStringsInner(item, seen));
|
|
30
|
+
}
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sanitized: Record<string, unknown> = {};
|
|
35
|
+
seen.set(value, sanitized);
|
|
36
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
37
|
+
sanitized[key.toWellFormed()] = sanitizeJsonStringsInner(nestedValue, seen);
|
|
38
|
+
}
|
|
39
|
+
return sanitized;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
export function toNumber(value: unknown): number | undefined {
|
|
15
43
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
16
44
|
if (typeof value === "string" && value.trim()) {
|