@oh-my-pi/pi-ai 17.0.4 → 17.0.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 +26 -0
- package/dist/types/auth-storage.d.ts +5 -5
- package/dist/types/providers/anthropic.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +2 -0
- package/dist/types/registry/api-key-login.d.ts +1 -1
- package/dist/types/registry/oauth/types.d.ts +3 -2
- package/dist/types/utils/schema/normalize.d.ts +15 -0
- package/dist/types/utils.d.ts +4 -3
- package/package.json +4 -4
- package/src/auth-storage.ts +128 -54
- package/src/providers/__tests__/kimi-code-thinking.test.ts +47 -0
- package/src/providers/amazon-bedrock.ts +1 -1
- package/src/providers/anthropic.ts +57 -15
- package/src/providers/cursor.ts +1 -1
- package/src/providers/devin.ts +95 -14
- package/src/providers/openai-codex-responses.ts +2 -1
- package/src/providers/openai-completions.ts +43 -7
- package/src/providers/openai-responses.ts +263 -164
- package/src/registry/api-key-login.ts +5 -2
- package/src/registry/moonshot.ts +7 -1
- package/src/registry/oauth/openai-codex.ts +22 -2
- package/src/registry/oauth/types.ts +3 -2
- package/src/utils/schema/normalize.ts +103 -11
- package/src/utils.ts +10 -5
|
@@ -100,6 +100,8 @@ export type AnthropicHeaderOptions = {
|
|
|
100
100
|
isCloudflareAiGateway?: boolean;
|
|
101
101
|
claudeCodeSessionId?: string;
|
|
102
102
|
claudeCodeBetas?: readonly string[];
|
|
103
|
+
/** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */
|
|
104
|
+
allowAnthropicHeaderOverrides?: boolean;
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
|
|
@@ -144,7 +146,8 @@ const claudeCodeAgentBetaDefaults = [
|
|
|
144
146
|
midConversationSystemBeta,
|
|
145
147
|
"advanced-tool-use-2025-11-20",
|
|
146
148
|
] as const;
|
|
147
|
-
const
|
|
149
|
+
const extendedCacheTtlBeta = "extended-cache-ttl-2025-04-11";
|
|
150
|
+
const claudeCodeAgentPostEffortBetas = [extendedCacheTtlBeta] as const;
|
|
148
151
|
const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
|
|
149
152
|
const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
|
|
150
153
|
// Asks the API to redact thinking blocks from responses. Only sent when the
|
|
@@ -225,22 +228,36 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
225
228
|
const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
|
|
226
229
|
const isCloudflare = options.isCloudflareAiGateway ?? false;
|
|
227
230
|
const honorAuthorization = !oauthToken && !isCloudflare;
|
|
231
|
+
const allowAnthropicHeaderOverrides =
|
|
232
|
+
oauthToken &&
|
|
233
|
+
options.allowAnthropicHeaderOverrides === true &&
|
|
234
|
+
!isCloudflare &&
|
|
235
|
+
!isOfficialAnthropicApiUrl(options.baseUrl);
|
|
228
236
|
const honorApiKey = !isCloudflare;
|
|
229
237
|
const modelHeaders: Record<string, string> = {};
|
|
238
|
+
const anthropicHeaderOverrides: Record<string, string> = {};
|
|
230
239
|
const filteredEnforcedKeys: string[] = [];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
const headerSource = options.modelHeaders;
|
|
241
|
+
if (headerSource) {
|
|
242
|
+
for (const key in headerSource) {
|
|
243
|
+
const value = headerSource[key];
|
|
244
|
+
const lowerKey = key.toLowerCase();
|
|
245
|
+
if (enforcedHeaderKeys.has(lowerKey)) {
|
|
246
|
+
if (allowAnthropicHeaderOverrides && overridableAnthropicHeaderKeys.has(lowerKey)) {
|
|
247
|
+
anthropicHeaderOverrides[key] = value;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// user-agent is always re-applied explicitly. authorization / x-api-key
|
|
251
|
+
// are silently re-applied in honoring branches and dropped + logged
|
|
252
|
+
// where the branch enforces its own credential.
|
|
253
|
+
if (lowerKey === "user-agent") continue;
|
|
254
|
+
if (lowerKey === "authorization" && honorAuthorization) continue;
|
|
255
|
+
if (lowerKey === "x-api-key" && honorApiKey) continue;
|
|
256
|
+
filteredEnforcedKeys.push(key);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
modelHeaders[key] = value;
|
|
242
260
|
}
|
|
243
|
-
modelHeaders[key] = value;
|
|
244
261
|
}
|
|
245
262
|
if (filteredEnforcedKeys.length > 0) {
|
|
246
263
|
// Caller/env-supplied values (options.headers, ANTHROPIC_CUSTOM_HEADERS)
|
|
@@ -266,7 +283,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
266
283
|
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
|
|
267
284
|
? incomingUserAgent
|
|
268
285
|
: `claude-cli/${claudeCodeVersion} (external, local-agent, agent-sdk/${claudeAgentSdkVersion})`;
|
|
269
|
-
|
|
286
|
+
const headers = {
|
|
270
287
|
...modelHeaders,
|
|
271
288
|
...claudeCodeHeaders,
|
|
272
289
|
Accept: acceptHeader,
|
|
@@ -278,6 +295,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
278
295
|
"User-Agent": userAgent,
|
|
279
296
|
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
280
297
|
};
|
|
298
|
+
return allowAnthropicHeaderOverrides ? mergeHeaders(headers, anthropicHeaderOverrides) : headers;
|
|
281
299
|
} else if (!isOfficialAnthropicApiUrl(options.baseUrl)) {
|
|
282
300
|
return {
|
|
283
301
|
...modelHeaders,
|
|
@@ -424,7 +442,15 @@ function getCacheControl(
|
|
|
424
442
|
cacheRetention: CacheRetention | undefined,
|
|
425
443
|
isOAuthToken: boolean,
|
|
426
444
|
): { retention: CacheRetention; cacheControl?: AnthropicCacheControl } {
|
|
427
|
-
|
|
445
|
+
// OAuth mirrors Claude Code and always defaults to 1h retention. API-key
|
|
446
|
+
// requests also default to 1h where the endpoint supports it (canonical
|
|
447
|
+
// Anthropic API, `compat.supportsLongCacheRetention`): agent sessions
|
|
448
|
+
// routinely idle past 5 minutes waiting on background jobs, and a 5m
|
|
449
|
+
// breakpoint cold-misses the entire prefix on resume. PI_CACHE_RETENTION
|
|
450
|
+
// still overrides the API-key default in either direction.
|
|
451
|
+
const retention = isOAuthToken
|
|
452
|
+
? (cacheRetention ?? "long")
|
|
453
|
+
: resolveCacheRetention(cacheRetention, model.compat.supportsLongCacheRetention ? "long" : "short");
|
|
428
454
|
if (retention === "none") {
|
|
429
455
|
return { retention };
|
|
430
456
|
}
|
|
@@ -512,6 +538,10 @@ const enforcedHeaderKeys = new Set(
|
|
|
512
538
|
].map(key => key.toLowerCase()),
|
|
513
539
|
);
|
|
514
540
|
|
|
541
|
+
const overridableAnthropicHeaderKeys = new Set(
|
|
542
|
+
[...Object.keys(claudeCodeHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()),
|
|
543
|
+
);
|
|
544
|
+
|
|
515
545
|
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
516
546
|
|
|
517
547
|
function createClaudeBillingHeader(firstUserMessageText: string): string {
|
|
@@ -1853,6 +1883,17 @@ const streamAnthropicOnce = (
|
|
|
1853
1883
|
) {
|
|
1854
1884
|
extraBetas.push(contextManagementBeta);
|
|
1855
1885
|
}
|
|
1886
|
+
// `ttl: "1h"` requires the extended-cache-ttl beta on API-key
|
|
1887
|
+
// requests. OAuth requests never add it here: agent requests
|
|
1888
|
+
// already carry it in the Claude Code beta list, and utility
|
|
1889
|
+
// requests must not deviate from CC's header fingerprint.
|
|
1890
|
+
if (
|
|
1891
|
+
!(options?.isOAuth ?? isAnthropicOAuthToken(apiKey)) &&
|
|
1892
|
+
getCacheControl(model, options?.cacheRetention, false).cacheControl?.ttl === "1h" &&
|
|
1893
|
+
!extraBetas.includes(extendedCacheTtlBeta)
|
|
1894
|
+
) {
|
|
1895
|
+
extraBetas.push(extendedCacheTtlBeta);
|
|
1896
|
+
}
|
|
1856
1897
|
// Server-side fallback beta chain: opt-in via `options.fallbacks`.
|
|
1857
1898
|
// Nested overrides (`speed`, `output_config.effort`,
|
|
1858
1899
|
// `output_config.task_budget`) reuse the same top-level betas
|
|
@@ -2791,6 +2832,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2791
2832
|
dynamicHeaders,
|
|
2792
2833
|
),
|
|
2793
2834
|
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
|
2835
|
+
allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides,
|
|
2794
2836
|
claudeCodeSessionId,
|
|
2795
2837
|
claudeCodeBetas: oauthToken
|
|
2796
2838
|
? buildClaudeCodeBetas(
|
package/src/providers/cursor.ts
CHANGED
package/src/providers/devin.ts
CHANGED
|
@@ -43,9 +43,11 @@ import type {
|
|
|
43
43
|
Tool,
|
|
44
44
|
ToolCall,
|
|
45
45
|
} from "../types";
|
|
46
|
+
import { isDemotedThinking } from "../utils/block-symbols";
|
|
46
47
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
47
48
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
48
49
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
50
|
+
import { transformMessages } from "./transform-messages";
|
|
49
51
|
|
|
50
52
|
/** Base host for Codeium/Windsurf's Cascade chat API (Connect protocol over HTTP/1.1). */
|
|
51
53
|
export const DEVIN_API_URL = "https://server.codeium.com";
|
|
@@ -78,6 +80,13 @@ const CONNECT_END_STREAM_FLAG = 0x02;
|
|
|
78
80
|
* fails fast instead of consuming memory.
|
|
79
81
|
*/
|
|
80
82
|
const MAX_CONNECT_FRAME_PAYLOAD = 16 * 1024 * 1024;
|
|
83
|
+
/**
|
|
84
|
+
* Recovery heuristic for opaque Devin `invalid_argument` trailers. This is not
|
|
85
|
+
* asserted to be the backend's hard limit: small requests can hit the same
|
|
86
|
+
* intermittent error, while compactable message history this large is likely
|
|
87
|
+
* to benefit from the existing context-overflow maintenance path.
|
|
88
|
+
*/
|
|
89
|
+
const LARGE_HISTORY_RECOVERY_BYTES = 512 * 1024;
|
|
81
90
|
|
|
82
91
|
export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
83
92
|
model: Model<"devin-agent">,
|
|
@@ -157,10 +166,14 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
157
166
|
const auth = await fetchDevinAuthMetadata(apiKey, baseUrl, fetchImpl, options?.signal);
|
|
158
167
|
const chatBaseUrl = auth.baseUrl ?? baseUrl;
|
|
159
168
|
const request = buildDevinChatRequest(model, context, options, apiKey, auth.userJwt);
|
|
160
|
-
logger.debug("devin: sending chat request", { model: model.id, tools: context.tools?.length ?? 0 });
|
|
161
|
-
|
|
162
169
|
const reqBytes = toBinary(GetChatMessageRequestSchema, request);
|
|
163
170
|
const gz = gzipSync(reqBytes);
|
|
171
|
+
logger.debug("devin: sending chat request", {
|
|
172
|
+
model: model.id,
|
|
173
|
+
tools: context.tools?.length ?? 0,
|
|
174
|
+
requestBytes: reqBytes.byteLength,
|
|
175
|
+
compressedBytes: gz.byteLength,
|
|
176
|
+
});
|
|
164
177
|
const frame = Buffer.alloc(5 + gz.length);
|
|
165
178
|
frame[0] = CONNECT_COMPRESSED_FLAG;
|
|
166
179
|
frame.writeUInt32BE(gz.length, 1);
|
|
@@ -223,7 +236,53 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
223
236
|
if (flag & CONNECT_END_STREAM_FLAG) {
|
|
224
237
|
const trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;
|
|
225
238
|
const trailerError = readConnectTrailerError(trailerBytes.toString("utf8").trim());
|
|
226
|
-
if (trailerError)
|
|
239
|
+
if (trailerError) {
|
|
240
|
+
const error = new AIError.ValidationError(trailerError.formatted);
|
|
241
|
+
if (
|
|
242
|
+
firstTokenTime === undefined &&
|
|
243
|
+
trailerError.code.toLowerCase() === "invalid_argument" &&
|
|
244
|
+
/\binternal error\b/i.test(trailerError.message)
|
|
245
|
+
) {
|
|
246
|
+
// The full protobuf also contains the system prompt and tool
|
|
247
|
+
// schemas, which history maintenance cannot shrink. Re-encode
|
|
248
|
+
// only the repeated history field before choosing recovery.
|
|
249
|
+
let activeTailCount = 0;
|
|
250
|
+
const lastRole = context.messages.at(-1)?.role;
|
|
251
|
+
if (lastRole === "user" || lastRole === "developer") {
|
|
252
|
+
activeTailCount = 1;
|
|
253
|
+
// A trailing developer message can accompany the current user
|
|
254
|
+
// prompt. Earlier user-role records may instead be flushed
|
|
255
|
+
// execution history and must remain eligible for compaction.
|
|
256
|
+
if (lastRole === "developer") {
|
|
257
|
+
for (let i = context.messages.length - 2; i >= 0; i--) {
|
|
258
|
+
const role = context.messages[i].role;
|
|
259
|
+
if (role !== "user" && role !== "developer") break;
|
|
260
|
+
activeTailCount++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const shrinkablePrompts =
|
|
265
|
+
activeTailCount > 0
|
|
266
|
+
? request.chatMessagePrompts.slice(0, -activeTailCount)
|
|
267
|
+
: request.chatMessagePrompts;
|
|
268
|
+
const historyBytes = toBinary(
|
|
269
|
+
GetChatMessageRequestSchema,
|
|
270
|
+
create(GetChatMessageRequestSchema, {
|
|
271
|
+
chatMessagePrompts: shrinkablePrompts,
|
|
272
|
+
}),
|
|
273
|
+
).byteLength;
|
|
274
|
+
if (historyBytes >= LARGE_HISTORY_RECOVERY_BYTES) {
|
|
275
|
+
AIError.attach(error, AIError.create(AIError.Flag.ContextOverflow));
|
|
276
|
+
logger.warn("devin: treating large-history invalid_argument as context overflow", {
|
|
277
|
+
model: model.id,
|
|
278
|
+
historyBytes,
|
|
279
|
+
requestBytes: reqBytes.byteLength,
|
|
280
|
+
compressedBytes: gz.byteLength,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
227
286
|
continue;
|
|
228
287
|
}
|
|
229
288
|
|
|
@@ -322,7 +381,8 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
322
381
|
output.usage.output = Number(msg.usage.outputTokens);
|
|
323
382
|
output.usage.cacheRead = Number(msg.usage.cacheReadTokens);
|
|
324
383
|
output.usage.cacheWrite = Number(msg.usage.cacheWriteTokens);
|
|
325
|
-
output.usage.totalTokens =
|
|
384
|
+
output.usage.totalTokens =
|
|
385
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
|
|
@@ -442,6 +502,7 @@ function buildDevinChatRequest(
|
|
|
442
502
|
options?.stopSequences && options.stopSequences.length > 0
|
|
443
503
|
? [...DEVIN_DEFAULT_STOP_PATTERNS, ...options.stopSequences]
|
|
444
504
|
: DEVIN_DEFAULT_STOP_PATTERNS;
|
|
505
|
+
const messages = transformMessages(context.messages, model);
|
|
445
506
|
return create(GetChatMessageRequestSchema, {
|
|
446
507
|
metadata: create(MetadataSchema, {
|
|
447
508
|
apiKey,
|
|
@@ -453,7 +514,7 @@ function buildDevinChatRequest(
|
|
|
453
514
|
locale: "en",
|
|
454
515
|
}),
|
|
455
516
|
prompt: (context.systemPrompt ?? []).join("\n\n"),
|
|
456
|
-
chatMessagePrompts: buildChatMessagePrompts(
|
|
517
|
+
chatMessagePrompts: buildChatMessagePrompts(messages, cascadeId, model),
|
|
457
518
|
chatModelUid: options?.chatModelUid ?? model.requestModelId ?? model.id,
|
|
458
519
|
requestType: ChatMessageRequestType.CASCADE,
|
|
459
520
|
plannerMode: ConversationalPlannerMode.DEFAULT,
|
|
@@ -485,7 +546,11 @@ function buildDevinChatRequest(
|
|
|
485
546
|
}
|
|
486
547
|
|
|
487
548
|
/** Map omp `Message` history onto Cascade `ChatMessagePrompt`s (USER / SYSTEM / TOOL channels). */
|
|
488
|
-
function buildChatMessagePrompts(
|
|
549
|
+
function buildChatMessagePrompts(
|
|
550
|
+
messages: Message[],
|
|
551
|
+
cascadeId: string,
|
|
552
|
+
model: Model<"devin-agent">,
|
|
553
|
+
): ChatMessagePrompt[] {
|
|
489
554
|
const prompts: ChatMessagePrompt[] = [];
|
|
490
555
|
// messageId seeds are `cascadeId\0index\0role[...]` — prompt text is excluded
|
|
491
556
|
// so ids stay stable across content edits / history rebuilds.
|
|
@@ -513,16 +578,18 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
513
578
|
}),
|
|
514
579
|
);
|
|
515
580
|
} else if (msg.role === "assistant") {
|
|
581
|
+
const isNativeDevinMessage =
|
|
582
|
+
msg.api === model.api && msg.provider === model.provider && msg.model === model.id;
|
|
516
583
|
let promptText = "";
|
|
517
584
|
let thinkingText = "";
|
|
518
585
|
let signature = "";
|
|
519
586
|
const toolCalls: ChatToolCall[] = [];
|
|
520
587
|
for (const part of msg.content) {
|
|
521
588
|
if (part.type === "text") {
|
|
522
|
-
promptText += part.text
|
|
589
|
+
promptText += `${part.text}${isDemotedThinking(part) ? "\n" : ""}`;
|
|
523
590
|
} else if (part.type === "thinking") {
|
|
524
591
|
thinkingText += part.thinking;
|
|
525
|
-
if (!signature && part.thinkingSignature) signature = part.thinkingSignature;
|
|
592
|
+
if (isNativeDevinMessage && !signature && part.thinkingSignature) signature = part.thinkingSignature;
|
|
526
593
|
} else if (part.type === "toolCall") {
|
|
527
594
|
toolCalls.push(
|
|
528
595
|
create(ChatToolCallSchema, {
|
|
@@ -533,9 +600,13 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
533
600
|
);
|
|
534
601
|
}
|
|
535
602
|
}
|
|
603
|
+
if (!promptText && !thinkingText && !signature && toolCalls.length === 0) continue;
|
|
536
604
|
prompts.push(
|
|
537
605
|
create(ChatMessagePromptSchema, {
|
|
538
|
-
messageId:
|
|
606
|
+
messageId:
|
|
607
|
+
isNativeDevinMessage && msg.responseId
|
|
608
|
+
? msg.responseId
|
|
609
|
+
: `bot-${deterministicUuid(`${cascadeId}\0${index}\0assistant`)}`,
|
|
539
610
|
source: ChatMessageSource.SYSTEM,
|
|
540
611
|
prompt: promptText,
|
|
541
612
|
thinking: thinkingText,
|
|
@@ -569,12 +640,18 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
569
640
|
return prompts;
|
|
570
641
|
}
|
|
571
642
|
|
|
643
|
+
interface ConnectTrailerError {
|
|
644
|
+
code: string;
|
|
645
|
+
message: string;
|
|
646
|
+
formatted: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
572
649
|
/**
|
|
573
|
-
* Parse a Connect end-of-stream JSON trailer and return
|
|
574
|
-
*
|
|
575
|
-
*
|
|
650
|
+
* Parse a Connect end-of-stream JSON trailer and return its structured error
|
|
651
|
+
* when it carries `{ error: { code, message } }`, else `null`. The trailer is
|
|
652
|
+
* untrusted server output, so the shape is checked with guards rather than asserted.
|
|
576
653
|
*/
|
|
577
|
-
function readConnectTrailerError(text: string):
|
|
654
|
+
function readConnectTrailerError(text: string): ConnectTrailerError | null {
|
|
578
655
|
if (text.length === 0) return null;
|
|
579
656
|
let parsed: unknown;
|
|
580
657
|
try {
|
|
@@ -588,5 +665,9 @@ function readConnectTrailerError(text: string): string | null {
|
|
|
588
665
|
const code = "code" in err && typeof err.code === "string" ? err.code : "";
|
|
589
666
|
const message = "message" in err && typeof err.message === "string" ? err.message : "";
|
|
590
667
|
if (!code && !message) return null;
|
|
591
|
-
return
|
|
668
|
+
return {
|
|
669
|
+
code,
|
|
670
|
+
message,
|
|
671
|
+
formatted: `Devin stream error${code ? ` ${code}` : ""}: ${message}`,
|
|
672
|
+
};
|
|
592
673
|
}
|
|
@@ -3913,7 +3913,8 @@ function redactHeaders(headers: Headers): Record<string, string> {
|
|
|
3913
3913
|
return redacted;
|
|
3914
3914
|
}
|
|
3915
3915
|
|
|
3916
|
-
|
|
3916
|
+
/** Resolve a Codex Responses endpoint exactly as the chat and compaction transports do. */
|
|
3917
|
+
export function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
|
|
3917
3918
|
const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : CODEX_BASE_URL;
|
|
3918
3919
|
const normalized = raw.replace(/\/+$/, "");
|
|
3919
3920
|
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
@@ -42,7 +42,13 @@ import {
|
|
|
42
42
|
import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
|
|
43
43
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
44
44
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
adaptSchemaForStrict,
|
|
47
|
+
NO_STRICT,
|
|
48
|
+
normalizeSchemaForMoonshot,
|
|
49
|
+
sanitizeSchemaForGrammar,
|
|
50
|
+
toolWireSchema,
|
|
51
|
+
} from "../utils/schema";
|
|
46
52
|
import {
|
|
47
53
|
type HealedToolCall,
|
|
48
54
|
StreamMarkupHealing,
|
|
@@ -671,7 +677,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
671
677
|
}
|
|
672
678
|
activeReasoningEffortFallbackKey = reasoningEffortFallbackKey;
|
|
673
679
|
activeRequestParams = params;
|
|
674
|
-
options?.onPayload?.(params);
|
|
680
|
+
options?.onPayload?.(params, model);
|
|
675
681
|
rawRequestDump = {
|
|
676
682
|
provider: model.provider,
|
|
677
683
|
api: output.api,
|
|
@@ -1431,6 +1437,20 @@ function dropOpenRouterKimiForcedToolReasoning(
|
|
|
1431
1437
|
}
|
|
1432
1438
|
}
|
|
1433
1439
|
|
|
1440
|
+
function hasActiveNativeKimiK3Reasoning(
|
|
1441
|
+
model: Model<"openai-completions">,
|
|
1442
|
+
options: OpenAICompletionsOptions | undefined,
|
|
1443
|
+
): boolean {
|
|
1444
|
+
if (model.provider !== "kimi-code" || model.id.toLowerCase() !== "k3" || !model.reasoning) return false;
|
|
1445
|
+
if (options?.reasoning === undefined || options.disableReasoning) return false;
|
|
1446
|
+
try {
|
|
1447
|
+
const url = new URL(model.baseUrl);
|
|
1448
|
+
return url.hostname === "api.kimi.com" && (url.pathname === "/coding" || url.pathname.startsWith("/coding/"));
|
|
1449
|
+
} catch {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1434
1454
|
function buildParams(
|
|
1435
1455
|
model: Model<"openai-completions">,
|
|
1436
1456
|
context: Context,
|
|
@@ -1518,6 +1538,20 @@ function buildParams(
|
|
|
1518
1538
|
) {
|
|
1519
1539
|
params.tool_choice = "required";
|
|
1520
1540
|
}
|
|
1541
|
+
const forcedToolName =
|
|
1542
|
+
typeof params.tool_choice === "object" && params.tool_choice !== null && "function" in params.tool_choice
|
|
1543
|
+
? params.tool_choice.function.name
|
|
1544
|
+
: undefined;
|
|
1545
|
+
if (
|
|
1546
|
+
forcedToolName !== undefined &&
|
|
1547
|
+
Array.isArray(params.tools) &&
|
|
1548
|
+
params.tools.some(tool => tool.type === "function" && tool.function.name === forcedToolName) &&
|
|
1549
|
+
hasActiveNativeKimiK3Reasoning(model, options)
|
|
1550
|
+
) {
|
|
1551
|
+
// Native K3 reasoning is incompatible with selecting a specific function.
|
|
1552
|
+
// Preserve the hard tool-use contract while letting K3 choose among tools.
|
|
1553
|
+
params.tool_choice = "required";
|
|
1554
|
+
}
|
|
1521
1555
|
if (isForcedToolChoice(params.tool_choice) && !initialCompat.supportsForcedToolChoice) {
|
|
1522
1556
|
// Some thinking-required OpenAI-compatible models reject forced
|
|
1523
1557
|
// `tool_choice` while still accepting tools with the default auto
|
|
@@ -1537,10 +1571,6 @@ function buildParams(
|
|
|
1537
1571
|
delete params.tool_choice;
|
|
1538
1572
|
}
|
|
1539
1573
|
|
|
1540
|
-
const forcedToolName =
|
|
1541
|
-
typeof params.tool_choice === "object" && params.tool_choice !== null && "function" in params.tool_choice
|
|
1542
|
-
? params.tool_choice.function.name
|
|
1543
|
-
: undefined;
|
|
1544
1574
|
if (
|
|
1545
1575
|
forcedToolName !== undefined &&
|
|
1546
1576
|
(!Array.isArray(params.tools) ||
|
|
@@ -2205,10 +2235,16 @@ function convertTools(
|
|
|
2205
2235
|
description: tool.description || "",
|
|
2206
2236
|
// Moonshot/Kimi native hosts validate against the stricter MFJS subset
|
|
2207
2237
|
// (const→enum, typed enums, no validators) and 400 otherwise.
|
|
2238
|
+
// Grammar-constrained local backends (llama.cpp, LM Studio, vLLM)
|
|
2239
|
+
// build a GBNF grammar from the schema and 400 with
|
|
2240
|
+
// `Unrecognized schema: true` on the bare boolean subschema
|
|
2241
|
+
// `toolWireSchema` emits for open fields (issue #5914).
|
|
2208
2242
|
parameters:
|
|
2209
2243
|
compat.toolSchemaFlavor === "moonshot-mfjs"
|
|
2210
2244
|
? (normalizeSchemaForMoonshot(wireParameters) as Record<string, unknown>)
|
|
2211
|
-
:
|
|
2245
|
+
: compat.toolSchemaFlavor === "grammar"
|
|
2246
|
+
? sanitizeSchemaForGrammar(wireParameters)
|
|
2247
|
+
: wireParameters,
|
|
2212
2248
|
// Only include strict if provider supports it. Some reject unknown fields.
|
|
2213
2249
|
...(includeStrict ? { strict: true } : includeExplicitFalse ? { strict: false } : {}),
|
|
2214
2250
|
},
|