@gajae-code/ai 0.2.0 → 0.2.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 +14 -1
- package/dist/types/providers/anthropic.d.ts +4 -1
- package/dist/types/providers/openai-request-transform.d.ts +4 -0
- package/dist/types/providers/transform-messages.d.ts +3 -1
- package/dist/types/types.d.ts +15 -1
- package/package.json +2 -2
- package/src/models.json +770 -51
- package/src/provider-models/descriptors.ts +1 -0
- package/src/providers/anthropic.ts +48 -4
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/openai-completions.ts +18 -3
- package/src/providers/openai-request-transform.ts +135 -0
- package/src/providers/openai-responses.ts +21 -3
- package/src/providers/transform-messages.ts +17 -7
- package/src/stream.ts +1 -0
- package/src/types.ts +17 -0
- package/src/utils/http-inspector.ts +36 -0
|
@@ -295,6 +295,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
295
295
|
export const DEFAULT_MODEL_PER_PROVIDER: Record<KnownProvider, string> = {
|
|
296
296
|
...Object.fromEntries(PROVIDER_DESCRIPTORS.map(d => [d.providerId, d.defaultModel])),
|
|
297
297
|
// Providers not in PROVIDER_DESCRIPTORS (special auth or no standard discovery)
|
|
298
|
+
"azure-openai": "gpt-4.1",
|
|
298
299
|
"alibaba-coding-plan": "qwen3.5-plus",
|
|
299
300
|
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
|
300
301
|
"google-antigravity": "gemini-3-pro-high",
|
|
@@ -304,6 +304,17 @@ export function isAnthropicFastModeUnsupportedError(error: unknown): boolean {
|
|
|
304
304
|
return false;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
export function isAnthropicThinkingBlockMutationError(error: unknown): boolean {
|
|
308
|
+
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
310
|
+
return (
|
|
311
|
+
/invalid_request_error/i.test(message) &&
|
|
312
|
+
/thinking|redacted_thinking/i.test(message) &&
|
|
313
|
+
/latest assistant message/i.test(message) &&
|
|
314
|
+
/cannot be modified/i.test(message)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
307
318
|
function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
|
|
308
319
|
const tools = params.tools as Array<{ strict?: unknown }> | undefined;
|
|
309
320
|
return tools?.some(tool => tool.strict === true) ?? false;
|
|
@@ -1058,8 +1069,18 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1058
1069
|
(providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
|
|
1059
1070
|
let strictFallbackErrorMessage: string | undefined;
|
|
1060
1071
|
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
1061
|
-
const prepareParams = async (
|
|
1062
|
-
|
|
1072
|
+
const prepareParams = async (paramsOptions?: {
|
|
1073
|
+
repairLatestAssistantThinking?: boolean;
|
|
1074
|
+
}): Promise<MessageCreateParamsStreaming> => {
|
|
1075
|
+
let nextParams = buildParams(
|
|
1076
|
+
model,
|
|
1077
|
+
baseUrl,
|
|
1078
|
+
context,
|
|
1079
|
+
isOAuthToken,
|
|
1080
|
+
options,
|
|
1081
|
+
disableStrictTools,
|
|
1082
|
+
paramsOptions?.repairLatestAssistantThinking === true,
|
|
1083
|
+
);
|
|
1063
1084
|
if (disableStrictTools) {
|
|
1064
1085
|
dropAnthropicStrictTools(nextParams);
|
|
1065
1086
|
}
|
|
@@ -1096,6 +1117,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1096
1117
|
// Provider-level transport/rate-limit failures: only before any streamed content starts.
|
|
1097
1118
|
// Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
|
|
1098
1119
|
let providerRetryAttempt = 0;
|
|
1120
|
+
let thinkingRepairAttempted = false;
|
|
1099
1121
|
while (true) {
|
|
1100
1122
|
activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
1101
1123
|
const firstEventTimeoutAbortError = new Error(
|
|
@@ -1372,6 +1394,26 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1372
1394
|
firstTokenTime = undefined;
|
|
1373
1395
|
continue;
|
|
1374
1396
|
}
|
|
1397
|
+
if (
|
|
1398
|
+
!thinkingRepairAttempted &&
|
|
1399
|
+
firstTokenTime === undefined &&
|
|
1400
|
+
isAnthropicThinkingBlockMutationError(streamFailure)
|
|
1401
|
+
) {
|
|
1402
|
+
logger.debug("anthropic: repairing latest assistant thinking replay after provider rejection", {
|
|
1403
|
+
model: model.id,
|
|
1404
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
1405
|
+
});
|
|
1406
|
+
thinkingRepairAttempted = true;
|
|
1407
|
+
params = await prepareParams({ repairLatestAssistantThinking: true });
|
|
1408
|
+
providerRetryAttempt = 0;
|
|
1409
|
+
output.content.length = 0;
|
|
1410
|
+
output.responseId = undefined;
|
|
1411
|
+
output.providerPayload = undefined;
|
|
1412
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
1413
|
+
output.stopReason = "stop";
|
|
1414
|
+
firstTokenTime = undefined;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1375
1417
|
if (
|
|
1376
1418
|
!dropFastMode &&
|
|
1377
1419
|
resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
|
|
@@ -1887,11 +1929,12 @@ function buildParams(
|
|
|
1887
1929
|
isOAuthToken: boolean,
|
|
1888
1930
|
options?: AnthropicOptions,
|
|
1889
1931
|
disableStrictTools = false,
|
|
1932
|
+
repairLatestAssistantThinking = false,
|
|
1890
1933
|
): MessageCreateParamsStreaming {
|
|
1891
1934
|
const { cacheControl } = getCacheControl(model, baseUrl, options?.cacheRetention);
|
|
1892
1935
|
const params: AnthropicSamplingParams = {
|
|
1893
1936
|
model: model.id,
|
|
1894
|
-
messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
|
|
1937
|
+
messages: convertAnthropicMessages(context.messages, model, isOAuthToken, { repairLatestAssistantThinking }),
|
|
1895
1938
|
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
1896
1939
|
stream: true,
|
|
1897
1940
|
};
|
|
@@ -2074,10 +2117,11 @@ export function convertAnthropicMessages(
|
|
|
2074
2117
|
messages: Message[],
|
|
2075
2118
|
model: Model<"anthropic-messages">,
|
|
2076
2119
|
isOAuthToken: boolean,
|
|
2120
|
+
options?: { repairLatestAssistantThinking?: boolean },
|
|
2077
2121
|
): MessageParam[] {
|
|
2078
2122
|
const params: MessageParam[] = [];
|
|
2079
2123
|
|
|
2080
|
-
const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
|
|
2124
|
+
const transformedMessages = transformMessages(messages, model, normalizeToolCallId, options);
|
|
2081
2125
|
|
|
2082
2126
|
for (let i = 0; i < transformedMessages.length; i++) {
|
|
2083
2127
|
const msg = transformedMessages[i];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
|
|
5
5
|
*/
|
|
6
6
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
7
|
-
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.
|
|
7
|
+
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.44.1";
|
|
8
8
|
const platform = process.platform === "win32" ? "win32" : process.platform;
|
|
9
9
|
const arch = process.arch === "x64" ? "x64" : process.arch;
|
|
10
10
|
return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
|
|
@@ -66,6 +66,11 @@ import {
|
|
|
66
66
|
resolveGitHubCopilotBaseUrl,
|
|
67
67
|
} from "./github-copilot-headers";
|
|
68
68
|
import { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "./openai-completions-compat";
|
|
69
|
+
import {
|
|
70
|
+
applyOpenAIRequestTransformBody,
|
|
71
|
+
applyOpenAIRequestTransformHeaders,
|
|
72
|
+
wrapFetchForOpenAIRequestTransform,
|
|
73
|
+
} from "./openai-request-transform";
|
|
69
74
|
import { createInitialResponsesAssistantMessage } from "./openai-responses-shared";
|
|
70
75
|
import { transformMessages } from "./transform-messages";
|
|
71
76
|
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
@@ -956,6 +961,7 @@ async function createClient(
|
|
|
956
961
|
if (model.provider === "kimi-code") {
|
|
957
962
|
headers = { ...getKimiCommonHeaders(), ...headers };
|
|
958
963
|
}
|
|
964
|
+
headers = applyOpenAIRequestTransformHeaders(headers, model.requestTransform, `Gajae-Code/${packageJson.version}`);
|
|
959
965
|
let copilotPremiumRequests: number | undefined;
|
|
960
966
|
|
|
961
967
|
let baseUrl =
|
|
@@ -1013,7 +1019,14 @@ async function createClient(
|
|
|
1013
1019
|
},
|
|
1014
1020
|
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
1015
1021
|
);
|
|
1016
|
-
const
|
|
1022
|
+
const transformedFetch = wrapFetchForOpenAIRequestTransform(
|
|
1023
|
+
wrappedFetch,
|
|
1024
|
+
model.requestTransform,
|
|
1025
|
+
`Gajae-Code/${packageJson.version}`,
|
|
1026
|
+
);
|
|
1027
|
+
const debugFetch = onSseEvent
|
|
1028
|
+
? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
|
|
1029
|
+
: transformedFetch;
|
|
1017
1030
|
// Bound HTTP request timeout to roughly the first-event watchdog window.
|
|
1018
1031
|
// The OpenAI SDK's default is 10 minutes per attempt × `maxRetries`, which
|
|
1019
1032
|
// turns a stalled-before-headers fetch into a multi-minute hang invisible
|
|
@@ -1078,11 +1091,12 @@ function buildParams(
|
|
|
1078
1091
|
const effectiveMaxTokens = options?.maxTokens ?? (isKimi ? model.maxTokens : undefined);
|
|
1079
1092
|
|
|
1080
1093
|
const requestModelId =
|
|
1081
|
-
model.
|
|
1094
|
+
model.wireModelId ??
|
|
1095
|
+
(model.provider === "fireworks"
|
|
1082
1096
|
? toFireworksWireModelId(model.id)
|
|
1083
1097
|
: model.provider === "firepass"
|
|
1084
1098
|
? toFirepassWireModelId(model.id)
|
|
1085
|
-
: model.id;
|
|
1099
|
+
: model.id);
|
|
1086
1100
|
const params: OpenAICompletionsParams = {
|
|
1087
1101
|
model: requestModelId,
|
|
1088
1102
|
messages,
|
|
@@ -1260,6 +1274,7 @@ function buildParams(
|
|
|
1260
1274
|
if (compat.extraBody) {
|
|
1261
1275
|
Object.assign(params, compat.extraBody);
|
|
1262
1276
|
}
|
|
1277
|
+
applyOpenAIRequestTransformBody(params, model.requestTransform);
|
|
1263
1278
|
|
|
1264
1279
|
return { params, toolStrictMode };
|
|
1265
1280
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { FetchImpl, ModelRequestTransform } from "../types";
|
|
2
|
+
|
|
3
|
+
const PROTECTED_EXTRA_BODY_KEYS = new Set([
|
|
4
|
+
"model",
|
|
5
|
+
"messages",
|
|
6
|
+
"input",
|
|
7
|
+
"instructions",
|
|
8
|
+
"stream",
|
|
9
|
+
"stream_options",
|
|
10
|
+
"store",
|
|
11
|
+
"max_tokens",
|
|
12
|
+
"max_completion_tokens",
|
|
13
|
+
"max_output_tokens",
|
|
14
|
+
"temperature",
|
|
15
|
+
"top_p",
|
|
16
|
+
"presence_penalty",
|
|
17
|
+
"frequency_penalty",
|
|
18
|
+
"reasoning",
|
|
19
|
+
"reasoning_effort",
|
|
20
|
+
"prompt_cache_key",
|
|
21
|
+
"prompt_cache_retention",
|
|
22
|
+
"service_tier",
|
|
23
|
+
"stop",
|
|
24
|
+
"tools",
|
|
25
|
+
"tool_choice",
|
|
26
|
+
"parallel_tool_calls",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const OPENAI_PROXY_STRIP_HEADERS = [
|
|
30
|
+
"x-stainless-arch",
|
|
31
|
+
"x-stainless-async",
|
|
32
|
+
"x-stainless-lang",
|
|
33
|
+
"x-stainless-os",
|
|
34
|
+
"x-stainless-package-version",
|
|
35
|
+
"x-stainless-retry-count",
|
|
36
|
+
"x-stainless-runtime",
|
|
37
|
+
"x-stainless-runtime-version",
|
|
38
|
+
"x-stainless-timeout",
|
|
39
|
+
"x-stainless-helper-method",
|
|
40
|
+
"openai-organization",
|
|
41
|
+
"openai-project",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
function resolveRequestTransform(
|
|
45
|
+
transform: ModelRequestTransform | undefined,
|
|
46
|
+
profileUserAgent: string,
|
|
47
|
+
): ModelRequestTransform | undefined {
|
|
48
|
+
if (!transform) return undefined;
|
|
49
|
+
const profileTransform: ModelRequestTransform =
|
|
50
|
+
transform.profile === "openai-proxy"
|
|
51
|
+
? {
|
|
52
|
+
stripHeaders: [...OPENAI_PROXY_STRIP_HEADERS],
|
|
53
|
+
setHeaders: { "User-Agent": profileUserAgent },
|
|
54
|
+
}
|
|
55
|
+
: {};
|
|
56
|
+
return {
|
|
57
|
+
...profileTransform,
|
|
58
|
+
...transform,
|
|
59
|
+
stripHeaders: transform.stripHeaders ?? profileTransform.stripHeaders,
|
|
60
|
+
setHeaders: transform.setHeaders
|
|
61
|
+
? { ...(profileTransform.setHeaders ?? {}), ...transform.setHeaders }
|
|
62
|
+
: profileTransform.setHeaders,
|
|
63
|
+
extraBody: transform.extraBody,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deleteHeaders(headers: Headers, names: readonly string[] | undefined): void {
|
|
68
|
+
for (const name of names ?? []) {
|
|
69
|
+
headers.delete(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setHeaders(headers: Headers, values: Record<string, string | null> | undefined): void {
|
|
74
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
75
|
+
if (value === null) {
|
|
76
|
+
headers.delete(name);
|
|
77
|
+
} else {
|
|
78
|
+
headers.set(name, value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function transformHeaders(
|
|
84
|
+
headers: RequestInit["headers"] | undefined,
|
|
85
|
+
transform: ModelRequestTransform | undefined,
|
|
86
|
+
): Headers {
|
|
87
|
+
const result = new Headers(headers);
|
|
88
|
+
deleteHeaders(result, transform?.stripHeaders);
|
|
89
|
+
setHeaders(result, transform?.setHeaders);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function applyOpenAIRequestTransformHeaders(
|
|
94
|
+
headers: Record<string, string>,
|
|
95
|
+
transform: ModelRequestTransform | undefined,
|
|
96
|
+
profileUserAgent: string,
|
|
97
|
+
): Record<string, string> {
|
|
98
|
+
const resolved = resolveRequestTransform(transform, profileUserAgent);
|
|
99
|
+
if (!resolved) return headers;
|
|
100
|
+
return Object.fromEntries(transformHeaders(headers, resolved).entries());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function applyOpenAIRequestTransformBody(params: object, transform: ModelRequestTransform | undefined): void {
|
|
104
|
+
if (!transform?.extraBody) return;
|
|
105
|
+
const body = params as Record<string, unknown>;
|
|
106
|
+
for (const [key, value] of Object.entries(transform.extraBody)) {
|
|
107
|
+
if (!PROTECTED_EXTRA_BODY_KEYS.has(key) && !(key in body)) {
|
|
108
|
+
body[key] = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function wrapFetchForOpenAIRequestTransform(
|
|
114
|
+
baseFetch: FetchImpl,
|
|
115
|
+
transform: ModelRequestTransform | undefined,
|
|
116
|
+
profileUserAgent: string,
|
|
117
|
+
): FetchImpl {
|
|
118
|
+
const resolved = resolveRequestTransform(transform, profileUserAgent);
|
|
119
|
+
if (!resolved) return baseFetch;
|
|
120
|
+
return Object.assign(
|
|
121
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
122
|
+
if (input instanceof Request) {
|
|
123
|
+
const request = new Request(input, init);
|
|
124
|
+
deleteHeaders(request.headers, resolved.stripHeaders);
|
|
125
|
+
setHeaders(request.headers, resolved.setHeaders);
|
|
126
|
+
return baseFetch(request);
|
|
127
|
+
}
|
|
128
|
+
return baseFetch(input, {
|
|
129
|
+
...init,
|
|
130
|
+
headers: transformHeaders(init?.headers, resolved),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ResponseCreateParamsStreaming,
|
|
6
6
|
ResponseInput,
|
|
7
7
|
} from "openai/resources/responses/responses";
|
|
8
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
8
9
|
import { getEnvApiKey } from "../stream";
|
|
9
10
|
import type {
|
|
10
11
|
AssistantMessage,
|
|
@@ -50,6 +51,11 @@ import {
|
|
|
50
51
|
resolveGitHubCopilotBaseUrl,
|
|
51
52
|
} from "./github-copilot-headers";
|
|
52
53
|
import { compactGrammarDefinition } from "./grammar";
|
|
54
|
+
import {
|
|
55
|
+
applyOpenAIRequestTransformBody,
|
|
56
|
+
applyOpenAIRequestTransformHeaders,
|
|
57
|
+
wrapFetchForOpenAIRequestTransform,
|
|
58
|
+
} from "./openai-request-transform";
|
|
53
59
|
import {
|
|
54
60
|
appendResponsesToolResultMessages,
|
|
55
61
|
applyCommonResponsesSamplingParams,
|
|
@@ -363,7 +369,11 @@ function createClient(
|
|
|
363
369
|
}
|
|
364
370
|
const rawApiKey = apiKey;
|
|
365
371
|
|
|
366
|
-
const headers =
|
|
372
|
+
const headers = applyOpenAIRequestTransformHeaders(
|
|
373
|
+
{ ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
|
|
374
|
+
model.requestTransform,
|
|
375
|
+
`Gajae-Code/${packageJson.version}`,
|
|
376
|
+
);
|
|
367
377
|
let copilotPremiumRequests: number | undefined;
|
|
368
378
|
|
|
369
379
|
let baseUrl =
|
|
@@ -390,6 +400,11 @@ function createClient(
|
|
|
390
400
|
headers["x-client-request-id"] ??= sessionId;
|
|
391
401
|
}
|
|
392
402
|
const baseFetch = fetchOverride ?? fetch;
|
|
403
|
+
const transformedFetch = wrapFetchForOpenAIRequestTransform(
|
|
404
|
+
baseFetch,
|
|
405
|
+
model.requestTransform,
|
|
406
|
+
`Gajae-Code/${packageJson.version}`,
|
|
407
|
+
);
|
|
393
408
|
return {
|
|
394
409
|
client: new OpenAI({
|
|
395
410
|
apiKey,
|
|
@@ -397,7 +412,9 @@ function createClient(
|
|
|
397
412
|
dangerouslyAllowBrowser: true,
|
|
398
413
|
maxRetries: 5,
|
|
399
414
|
defaultHeaders: headers,
|
|
400
|
-
fetch: onSseEvent
|
|
415
|
+
fetch: onSseEvent
|
|
416
|
+
? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
|
|
417
|
+
: transformedFetch,
|
|
401
418
|
}),
|
|
402
419
|
copilotPremiumRequests,
|
|
403
420
|
baseUrl,
|
|
@@ -453,7 +470,7 @@ function buildParams(
|
|
|
453
470
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
454
471
|
const promptCacheKey = getOpenAIResponsesCacheSessionId(options);
|
|
455
472
|
const params: OpenAIResponsesSamplingParams = {
|
|
456
|
-
model: model.id,
|
|
473
|
+
model: model.wireModelId ?? model.id,
|
|
457
474
|
input: messages,
|
|
458
475
|
instructions: systemInstructions,
|
|
459
476
|
stream: true,
|
|
@@ -490,6 +507,7 @@ function buildParams(
|
|
|
490
507
|
applyResponsesReasoningParams(params, model, options, messages, effort =>
|
|
491
508
|
mapReasoningEffort(effort as NonNullable<OpenAIResponsesOptions["reasoning"]>, model.compat?.reasoningEffortMap),
|
|
492
509
|
);
|
|
510
|
+
applyOpenAIRequestTransformBody(params, model.requestTransform);
|
|
493
511
|
|
|
494
512
|
return { conversationMessages, params };
|
|
495
513
|
}
|
|
@@ -31,6 +31,7 @@ export function transformMessages<TApi extends Api>(
|
|
|
31
31
|
messages: Message[],
|
|
32
32
|
model: Model<TApi>,
|
|
33
33
|
normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string,
|
|
34
|
+
options?: { repairLatestAssistantThinking?: boolean },
|
|
34
35
|
): Message[] {
|
|
35
36
|
// Build a map of original tool call IDs to normalized IDs
|
|
36
37
|
const toolCallIdMap = new Map<string, string>();
|
|
@@ -64,16 +65,24 @@ export function transformMessages<TApi extends Api>(
|
|
|
64
65
|
index === latestAssistantIndex &&
|
|
65
66
|
model.api === "anthropic-messages" &&
|
|
66
67
|
assistantMsg.api === "anthropic-messages";
|
|
67
|
-
// Aborted/errored messages may
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
68
|
+
// Aborted/errored messages may contain partially-streamed thinking blocks.
|
|
69
|
+
// Anthropic requires thinking/redacted_thinking bytes in replayed assistant
|
|
70
|
+
// messages to match the original response exactly; stripping a signature,
|
|
71
|
+
// well-forming text, or keeping a partial redacted block would emit a
|
|
72
|
+
// modified thinking sequence. Drop those private blocks instead. Tool calls
|
|
73
|
+
// are kept so the second pass can either preserve real results or synthesize
|
|
74
|
+
// an explicit aborted result without leaving dangling tool_use blocks.
|
|
75
|
+
const hasPartialThinking = assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error";
|
|
76
|
+
const dropLatestAssistantThinking =
|
|
77
|
+
options?.repairLatestAssistantThinking === true &&
|
|
78
|
+
index === latestAssistantIndex &&
|
|
79
|
+
model.api === "anthropic-messages" &&
|
|
80
|
+
assistantMsg.api === "anthropic-messages";
|
|
71
81
|
|
|
72
82
|
const transformedContent = assistantMsg.content.flatMap(block => {
|
|
73
83
|
if (block.type === "thinking") {
|
|
74
|
-
|
|
75
|
-
const sanitized =
|
|
76
|
-
hasInvalidSignatures && block.thinkingSignature ? { ...block, thinkingSignature: undefined } : block;
|
|
84
|
+
if (hasPartialThinking || dropLatestAssistantThinking) return [];
|
|
85
|
+
const sanitized = block;
|
|
77
86
|
if (mustPreserveLatestAnthropicThinking) return sanitized;
|
|
78
87
|
// For same model: keep thinking blocks with signatures (needed for replay)
|
|
79
88
|
// even if the thinking text is empty (OpenAI encrypted reasoning)
|
|
@@ -88,6 +97,7 @@ export function transformMessages<TApi extends Api>(
|
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
if (block.type === "redactedThinking") {
|
|
100
|
+
if (hasPartialThinking || dropLatestAssistantThinking) return [];
|
|
91
101
|
if (mustPreserveLatestAnthropicThinking) return block;
|
|
92
102
|
if (isSameModel) return block;
|
|
93
103
|
return [];
|
package/src/stream.ts
CHANGED
|
@@ -97,6 +97,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
97
97
|
cursor: "CURSOR_ACCESS_TOKEN",
|
|
98
98
|
deepseek: "DEEPSEEK_API_KEY",
|
|
99
99
|
"openai-codex": "OPENAI_CODEX_OAUTH_TOKEN",
|
|
100
|
+
"azure-openai": "AZURE_OPENAI_API_KEY",
|
|
100
101
|
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
|
101
102
|
exa: "EXA_API_KEY",
|
|
102
103
|
jina: "JINA_API_KEY",
|
package/src/types.ts
CHANGED
|
@@ -98,6 +98,7 @@ export interface ThinkingConfig {
|
|
|
98
98
|
export type KnownProvider =
|
|
99
99
|
| "alibaba-coding-plan"
|
|
100
100
|
| "amazon-bedrock"
|
|
101
|
+
| "azure-openai"
|
|
101
102
|
| "anthropic"
|
|
102
103
|
| "google"
|
|
103
104
|
| "google-gemini-cli"
|
|
@@ -823,6 +824,18 @@ export interface VercelGatewayRouting {
|
|
|
823
824
|
}
|
|
824
825
|
|
|
825
826
|
// Model interface for the unified model system
|
|
827
|
+
|
|
828
|
+
export interface ModelRequestTransform {
|
|
829
|
+
/** Named request-shaping preset. `openai-proxy` removes OpenAI SDK telemetry headers and uses a generic Gajae-Code User-Agent. */
|
|
830
|
+
profile?: "openai-proxy";
|
|
831
|
+
/** Header names to remove from the final outbound request. Case-insensitive. */
|
|
832
|
+
stripHeaders?: string[];
|
|
833
|
+
/** Headers to set after stripping; use null to remove a header explicitly. */
|
|
834
|
+
setHeaders?: Record<string, string | null>;
|
|
835
|
+
/** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
|
|
836
|
+
extraBody?: Record<string, unknown>;
|
|
837
|
+
}
|
|
838
|
+
|
|
826
839
|
export interface Model<TApi extends Api = any> {
|
|
827
840
|
id: string;
|
|
828
841
|
name: string;
|
|
@@ -861,6 +874,10 @@ export interface Model<TApi extends Api = any> {
|
|
|
861
874
|
preferWebsockets?: boolean;
|
|
862
875
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
|
863
876
|
contextPromotionTarget?: string;
|
|
877
|
+
/** Provider-facing model id when it differs from the local selector id. */
|
|
878
|
+
wireModelId?: string;
|
|
879
|
+
/** Declarative request shaping for OpenAI-compatible proxy providers. */
|
|
880
|
+
requestTransform?: ModelRequestTransform;
|
|
864
881
|
/** Provider-assigned priority value (lower = higher priority). */
|
|
865
882
|
priority?: number;
|
|
866
883
|
/** Canonical thinking capability metadata for this model. */
|
|
@@ -102,9 +102,45 @@ function sanitizeDump(dump: RawHttpRequestDump): RawHttpRequestDump {
|
|
|
102
102
|
return {
|
|
103
103
|
...dump,
|
|
104
104
|
headers: redactHeaders(dump.headers),
|
|
105
|
+
body: sanitizeDumpBody(dump.body),
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function sanitizeDumpBody(value: unknown): unknown {
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return value.map(item => sanitizeDumpBody(item));
|
|
112
|
+
}
|
|
113
|
+
if (!isObject(value)) {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const type = typeof value.type === "string" ? value.type : undefined;
|
|
118
|
+
const redactedKeys = getRedactedBodyKeys(type);
|
|
119
|
+
const sanitized: Record<string, unknown> = {};
|
|
120
|
+
for (const [key, property] of Object.entries(value)) {
|
|
121
|
+
if (redactedKeys.has(key)) {
|
|
122
|
+
sanitized[key] = "[redacted]";
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
sanitized[key] = sanitizeDumpBody(property);
|
|
126
|
+
}
|
|
127
|
+
return sanitized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getRedactedBodyKeys(type: string | undefined): Set<string> {
|
|
131
|
+
const keys = new Set<string>();
|
|
132
|
+
if (type === "thinking") {
|
|
133
|
+
keys.add("thinking");
|
|
134
|
+
keys.add("signature");
|
|
135
|
+
keys.add("thinkingSignature");
|
|
136
|
+
keys.add("thoughtSignature");
|
|
137
|
+
}
|
|
138
|
+
if (type === "redacted_thinking" || type === "redactedThinking") {
|
|
139
|
+
keys.add("data");
|
|
140
|
+
}
|
|
141
|
+
return keys;
|
|
142
|
+
}
|
|
143
|
+
|
|
108
144
|
function redactHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
109
145
|
if (!headers) {
|
|
110
146
|
return undefined;
|