@oh-my-pi/pi-ai 16.0.0 → 16.0.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 +31 -0
- package/README.md +3 -0
- package/dist/types/providers/anthropic-client.d.ts +2 -0
- package/dist/types/providers/openai-responses.d.ts +39 -3
- package/dist/types/registry/oauth/openai-codex.d.ts +11 -1
- package/dist/types/registry/registry.d.ts +4 -0
- package/dist/types/registry/umans.d.ts +7 -0
- package/dist/types/utils/overflow.d.ts +2 -1
- package/dist/types/utils/schema/index.d.ts +1 -0
- package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
- package/package.json +3 -3
- package/src/dialect/rendering.ts +56 -1
- package/src/providers/anthropic-client.ts +5 -2
- package/src/providers/anthropic.ts +111 -16
- package/src/providers/azure-openai-responses.ts +5 -2
- package/src/providers/cursor.ts +4 -2
- package/src/providers/google-shared.ts +6 -3
- package/src/providers/openai-codex-responses.ts +20 -5
- package/src/providers/openai-completions.ts +121 -19
- package/src/providers/openai-responses-shared.ts +70 -13
- package/src/providers/openai-responses.ts +65 -15
- package/src/registry/oauth/openai-codex.ts +30 -13
- package/src/registry/registry.ts +2 -0
- package/src/registry/umans.ts +23 -0
- package/src/utils/overflow.ts +5 -2
- package/src/utils/schema/index.ts +1 -0
- package/src/utils/schema/normalize.ts +40 -3
- package/src/utils/schema/strict-tool-validation.ts +117 -0
- package/src/utils/schema/wire.ts +18 -3
- package/src/utils/validation.ts +159 -0
|
@@ -686,6 +686,14 @@ function resetOutputState(output: AssistantMessage): void {
|
|
|
686
686
|
output.stopReason = "stop";
|
|
687
687
|
output.stopDetails = undefined;
|
|
688
688
|
}
|
|
689
|
+
async function applyCodexPayloadReplacement<T extends Record<string, unknown>>(
|
|
690
|
+
model: Model<"openai-codex-responses">,
|
|
691
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
692
|
+
payload: T,
|
|
693
|
+
): Promise<T> {
|
|
694
|
+
const replacementPayload = await options?.onPayload?.(payload, model);
|
|
695
|
+
return replacementPayload !== undefined ? (replacementPayload as T) : payload;
|
|
696
|
+
}
|
|
689
697
|
|
|
690
698
|
function removeTransientBlockIndices(output: AssistantMessage): void {
|
|
691
699
|
for (const block of output.content) {
|
|
@@ -742,7 +750,6 @@ async function buildCodexRequestContext(
|
|
|
742
750
|
const promptCacheKey = resolveCodexPromptCacheKey(options);
|
|
743
751
|
const transportSessionId = resolveCodexTransportSessionId(options);
|
|
744
752
|
const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
|
|
745
|
-
options?.onPayload?.(transformedBody);
|
|
746
753
|
|
|
747
754
|
const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
|
|
748
755
|
const rawRequestDump: RawHttpRequestDump = {
|
|
@@ -878,6 +885,8 @@ async function openInitialCodexEventStream(
|
|
|
878
885
|
while (true) {
|
|
879
886
|
try {
|
|
880
887
|
return await openCodexWebSocketTransport(
|
|
888
|
+
model,
|
|
889
|
+
options,
|
|
881
890
|
requestContext,
|
|
882
891
|
requestSetup,
|
|
883
892
|
websocketState,
|
|
@@ -910,6 +919,8 @@ async function openInitialCodexEventStream(
|
|
|
910
919
|
return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
|
|
911
920
|
}
|
|
912
921
|
async function openCodexWebSocketTransport(
|
|
922
|
+
model: Model<"openai-codex-responses">,
|
|
923
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
913
924
|
requestContext: CodexRequestContext,
|
|
914
925
|
requestSetup: CodexRequestSetup,
|
|
915
926
|
websocketState: CodexWebSocketSessionState,
|
|
@@ -923,7 +934,7 @@ async function openCodexWebSocketTransport(
|
|
|
923
934
|
const chainedBody = buildCodexChainedRequestBody(requestContext.transformedBody, websocketState);
|
|
924
935
|
// WebSocket frames cannot carry per-request HTTP headers, so the Responses
|
|
925
936
|
// Lite marker rides in `client_metadata` on every `response.create`.
|
|
926
|
-
const websocketRequest
|
|
937
|
+
const websocketRequest = await applyCodexPayloadReplacement(model, options, {
|
|
927
938
|
type: "response.create",
|
|
928
939
|
...chainedBody,
|
|
929
940
|
...(requestContext.responsesLite
|
|
@@ -934,7 +945,7 @@ async function openCodexWebSocketTransport(
|
|
|
934
945
|
},
|
|
935
946
|
}
|
|
936
947
|
: {}),
|
|
937
|
-
};
|
|
948
|
+
});
|
|
938
949
|
const websocketHeaders = createCodexHeaders(
|
|
939
950
|
requestContext.requestHeaders,
|
|
940
951
|
requestContext.accountId,
|
|
@@ -945,6 +956,7 @@ async function openCodexWebSocketTransport(
|
|
|
945
956
|
requestContext.responsesLite,
|
|
946
957
|
);
|
|
947
958
|
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
|
|
959
|
+
requestContext.rawRequestDump.body = websocketRequest;
|
|
948
960
|
logCodexDebug("codex websocket request", {
|
|
949
961
|
url: toWebSocketUrl(requestContext.url),
|
|
950
962
|
model: requestContext.transformedBody.model,
|
|
@@ -1022,8 +1034,9 @@ async function openCodexSseTransport(
|
|
|
1022
1034
|
),
|
|
1023
1035
|
);
|
|
1024
1036
|
};
|
|
1025
|
-
|
|
1026
|
-
|
|
1037
|
+
const wireBody = await applyCodexPayloadReplacement(model, options, body);
|
|
1038
|
+
recordCodexWebSocketRequestStats(state, wireBody);
|
|
1039
|
+
return { eventStream: await open(wireBody), requestBodyForState: structuredCloneJSON(wireBody), transport: "sse" };
|
|
1027
1040
|
}
|
|
1028
1041
|
|
|
1029
1042
|
async function reopenCodexWebSocketRuntimeStream(
|
|
@@ -1033,6 +1046,8 @@ async function reopenCodexWebSocketRuntimeStream(
|
|
|
1033
1046
|
): Promise<void> {
|
|
1034
1047
|
try {
|
|
1035
1048
|
const next = await openCodexWebSocketTransport(
|
|
1049
|
+
context.model,
|
|
1050
|
+
context.options,
|
|
1036
1051
|
context.requestContext,
|
|
1037
1052
|
context.requestSetup,
|
|
1038
1053
|
state,
|
|
@@ -186,6 +186,105 @@ function serializeToolArguments(value: unknown): string {
|
|
|
186
186
|
return "{}";
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
function isUnsafeToolArgumentKey(key: string): boolean {
|
|
190
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isStreamingArgumentObject(value: unknown): value is Record<string, unknown> {
|
|
194
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function cloneStreamingArgumentValue(value: unknown): unknown {
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
return value.map(cloneStreamingArgumentValue);
|
|
200
|
+
}
|
|
201
|
+
if (isStreamingArgumentObject(value)) {
|
|
202
|
+
return mergeStreamingArgumentObjects(undefined, value);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function streamingArgumentValuesEqual(left: unknown, right: unknown): boolean {
|
|
208
|
+
if (left === right) return true;
|
|
209
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
210
|
+
if (left.length !== right.length) return false;
|
|
211
|
+
for (let i = 0; i < left.length; i++) {
|
|
212
|
+
if (!streamingArgumentValuesEqual(left[i], right[i])) return false;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (isStreamingArgumentObject(left) && isStreamingArgumentObject(right)) {
|
|
217
|
+
let leftKeys = 0;
|
|
218
|
+
for (const key in left) {
|
|
219
|
+
if (!Object.hasOwn(left, key) || isUnsafeToolArgumentKey(key)) continue;
|
|
220
|
+
leftKeys++;
|
|
221
|
+
if (!Object.hasOwn(right, key) || !streamingArgumentValuesEqual(left[key], right[key])) return false;
|
|
222
|
+
}
|
|
223
|
+
let rightKeys = 0;
|
|
224
|
+
for (const key in right) {
|
|
225
|
+
if (!Object.hasOwn(right, key) || isUnsafeToolArgumentKey(key)) continue;
|
|
226
|
+
rightKeys++;
|
|
227
|
+
}
|
|
228
|
+
return leftKeys === rightKeys;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function streamingArgumentArrayStartsWith(value: unknown[], prefix: unknown[]): boolean {
|
|
234
|
+
if (prefix.length > value.length) return false;
|
|
235
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
236
|
+
if (!streamingArgumentValuesEqual(value[i], prefix[i])) return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function mergeStreamingArgumentArrays(prev: unknown[], fragment: unknown[]): unknown[] {
|
|
242
|
+
if (streamingArgumentArrayStartsWith(fragment, prev)) {
|
|
243
|
+
return fragment.map(cloneStreamingArgumentValue);
|
|
244
|
+
}
|
|
245
|
+
if (streamingArgumentArrayStartsWith(prev, fragment)) {
|
|
246
|
+
return prev.map(cloneStreamingArgumentValue);
|
|
247
|
+
}
|
|
248
|
+
const merged = prev.map(cloneStreamingArgumentValue);
|
|
249
|
+
for (const value of fragment) {
|
|
250
|
+
merged.push(cloneStreamingArgumentValue(value));
|
|
251
|
+
}
|
|
252
|
+
return merged;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function mergeStreamingArgumentValues(prev: unknown, fragment: unknown): unknown {
|
|
256
|
+
if (typeof prev === "string" && typeof fragment === "string") {
|
|
257
|
+
return fragment.startsWith(prev) ? fragment : prev + fragment;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(prev) && Array.isArray(fragment)) {
|
|
260
|
+
return mergeStreamingArgumentArrays(prev, fragment);
|
|
261
|
+
}
|
|
262
|
+
if (isStreamingArgumentObject(prev) && isStreamingArgumentObject(fragment)) {
|
|
263
|
+
return mergeStreamingArgumentObjects(prev, fragment);
|
|
264
|
+
}
|
|
265
|
+
return cloneStreamingArgumentValue(fragment);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function mergeStreamingArgumentObjects(
|
|
269
|
+
prev: Record<string, unknown> | undefined,
|
|
270
|
+
fragment: Record<string, unknown>,
|
|
271
|
+
): Record<string, unknown> {
|
|
272
|
+
const merged: Record<string, unknown> = {};
|
|
273
|
+
if (prev) {
|
|
274
|
+
for (const key in prev) {
|
|
275
|
+
if (!Object.hasOwn(prev, key) || isUnsafeToolArgumentKey(key)) continue;
|
|
276
|
+
merged[key] = cloneStreamingArgumentValue(prev[key]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const key in fragment) {
|
|
280
|
+
if (!Object.hasOwn(fragment, key) || isUnsafeToolArgumentKey(key)) continue;
|
|
281
|
+
merged[key] = Object.hasOwn(merged, key)
|
|
282
|
+
? mergeStreamingArgumentValues(merged[key], fragment[key])
|
|
283
|
+
: cloneStreamingArgumentValue(fragment[key]);
|
|
284
|
+
}
|
|
285
|
+
return merged;
|
|
286
|
+
}
|
|
287
|
+
|
|
189
288
|
/**
|
|
190
289
|
* Check if conversation messages contain tool calls or tool results.
|
|
191
290
|
* This is needed because Anthropic (via proxy) requires the tools param
|
|
@@ -981,31 +1080,17 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
981
1080
|
// OpenAI JSON-string contract. Most chunks carry the complete object in one delta,
|
|
982
1081
|
// but cannot rely on that: replacing per-chunk drops earlier keys (and earlier
|
|
983
1082
|
// string content for the same key) when the host fragments the args across deltas.
|
|
984
|
-
//
|
|
985
|
-
// cumulative-vs-delta semantics
|
|
986
|
-
//
|
|
987
|
-
// behaviour for the common single-chunk shape (no prior value to merge with).
|
|
1083
|
+
// Deep-merge into the accumulated object. Strings and arrays detect
|
|
1084
|
+
// cumulative-vs-delta semantics by prefix, nested objects merge by key, and
|
|
1085
|
+
// prototype-polluting keys are ignored before storing or comparing values.
|
|
988
1086
|
//
|
|
989
1087
|
// `delta` stays empty here: emitting `JSON.stringify(rawArgs)` per chunk feeds
|
|
990
1088
|
// downstream concat-based accumulators (proxy.ts, openai-chat-server,
|
|
991
1089
|
// openai-responses-server, anthropic-messages-server) an invalid sequence like
|
|
992
1090
|
// `{"input":"a"}{"input":"b"}`. The merged object is flushed as a single
|
|
993
1091
|
// concat-safe delta in `finishToolCallBlock` before `toolcall_end` instead.
|
|
994
|
-
const prev =
|
|
995
|
-
|
|
996
|
-
typeof block.partialArgs === "object" &&
|
|
997
|
-
!Array.isArray(block.partialArgs)
|
|
998
|
-
? (block.partialArgs as Record<string, unknown>)
|
|
999
|
-
: undefined;
|
|
1000
|
-
const merged: Record<string, unknown> = prev ? { ...prev } : {};
|
|
1001
|
-
for (const [key, value] of Object.entries(rawArgs)) {
|
|
1002
|
-
const prevValue = merged[key];
|
|
1003
|
-
if (typeof prevValue === "string" && typeof value === "string") {
|
|
1004
|
-
merged[key] = value.startsWith(prevValue) ? value : prevValue + value;
|
|
1005
|
-
} else {
|
|
1006
|
-
merged[key] = value;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1092
|
+
const prev = isStreamingArgumentObject(block.partialArgs) ? block.partialArgs : undefined;
|
|
1093
|
+
const merged = mergeStreamingArgumentObjects(prev, rawArgs);
|
|
1009
1094
|
block.partialArgs = merged;
|
|
1010
1095
|
block.arguments = merged;
|
|
1011
1096
|
}
|
|
@@ -1077,6 +1162,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
1077
1162
|
output.stopReason = "toolUse";
|
|
1078
1163
|
}
|
|
1079
1164
|
|
|
1165
|
+
if (model.provider === "ollama" && output.stopReason === "length" && !hasVisibleCompletionContent(output)) {
|
|
1166
|
+
output.stopReason = "error";
|
|
1167
|
+
output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
|
|
1168
|
+
}
|
|
1080
1169
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
1081
1170
|
if (firstEventTimeoutError) {
|
|
1082
1171
|
throw firstEventTimeoutError;
|
|
@@ -2111,6 +2200,19 @@ function shouldRetryWithoutStrictTools(
|
|
|
2111
2200
|
);
|
|
2112
2201
|
}
|
|
2113
2202
|
|
|
2203
|
+
const NON_WHITESPACE_RE = /\S/;
|
|
2204
|
+
|
|
2205
|
+
function hasVisibleCompletionContent(message: AssistantMessage): boolean {
|
|
2206
|
+
for (const block of message.content) {
|
|
2207
|
+
if (block.type === "toolCall") return true;
|
|
2208
|
+
if (block.type === "text" && NON_WHITESPACE_RE.test(block.text)) return true;
|
|
2209
|
+
}
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
|
|
2214
|
+
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
|
|
2215
|
+
|
|
2114
2216
|
function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
|
|
2115
2217
|
stopReason: StopReason;
|
|
2116
2218
|
errorMessage?: string;
|
|
@@ -490,26 +490,37 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
490
490
|
// function_call deltas interleaved, and a singleton `current` reference would
|
|
491
491
|
// fold them into the wrong block and drop arguments on every call but the last.
|
|
492
492
|
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// `
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// identifier but not the other.
|
|
493
|
+
// OpenAI-compatible hosts can compound this by omitting `item.id` and
|
|
494
|
+
// `output_index` on `output_item.added` while routing later argument deltas to
|
|
495
|
+
// either the bare `call_id` or a synthesized `fc_<call_id>` item id. Register
|
|
496
|
+
// both keys so each delta reaches its own block instead of falling back to the
|
|
497
|
+
// most recently added parallel call.
|
|
499
498
|
const openItemsByOutputIndex = new Map<number, StreamingItem>();
|
|
500
499
|
const openItemsByItemId = new Map<string, StreamingItem>();
|
|
500
|
+
const openItemsByPrefixedCallId = new Map<string, StreamingItem>();
|
|
501
501
|
let lastOpenItem: StreamingItem | null = null;
|
|
502
502
|
const openItemsInOrder: StreamingItem[] = [];
|
|
503
503
|
|
|
504
|
+
const prefixedFunctionCallItemKey = (callId: string | undefined): string | undefined =>
|
|
505
|
+
callId ? `fc_${callId}` : undefined;
|
|
506
|
+
|
|
504
507
|
const registerOpenItem = (
|
|
505
508
|
outputIndex: number | undefined,
|
|
506
509
|
itemId: string | undefined,
|
|
507
510
|
entry: StreamingItem,
|
|
508
511
|
alternateItemKey?: string,
|
|
512
|
+
prefixedAlternateItemKey?: string,
|
|
509
513
|
): void => {
|
|
510
514
|
if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
|
|
511
515
|
if (itemId) openItemsByItemId.set(itemId, entry);
|
|
512
516
|
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.set(alternateItemKey, entry);
|
|
517
|
+
if (
|
|
518
|
+
prefixedAlternateItemKey &&
|
|
519
|
+
prefixedAlternateItemKey !== itemId &&
|
|
520
|
+
prefixedAlternateItemKey !== alternateItemKey
|
|
521
|
+
) {
|
|
522
|
+
openItemsByPrefixedCallId.set(prefixedAlternateItemKey, entry);
|
|
523
|
+
}
|
|
513
524
|
openItemsInOrder.push(entry);
|
|
514
525
|
lastOpenItem = entry;
|
|
515
526
|
};
|
|
@@ -527,11 +538,36 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
527
538
|
};
|
|
528
539
|
const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
|
|
529
540
|
typeof event.output_index === "number" || event.item_id !== undefined;
|
|
541
|
+
const lookupOpenToolCallAlias = (
|
|
542
|
+
event: { output_index?: number; item_id?: string },
|
|
543
|
+
type: "function_call" | "custom_tool_call",
|
|
544
|
+
): StreamingItem | undefined => {
|
|
545
|
+
if (typeof event.output_index === "number") {
|
|
546
|
+
const byOutputIndex = openItemsByOutputIndex.get(event.output_index);
|
|
547
|
+
if (byOutputIndex) return byOutputIndex;
|
|
548
|
+
// A lossy host (llama.cpp/Ollama, issue #2015) can omit `output_index` on
|
|
549
|
+
// `output_item.added` while still stamping the spec-required field on the
|
|
550
|
+
// delta. The index was never registered, so fall through to the prefixed
|
|
551
|
+
// alias / exact item-id maps instead of dropping to `lastOpenItem`.
|
|
552
|
+
}
|
|
553
|
+
if (event.item_id) {
|
|
554
|
+
// Prefixed call-id aliases share the same wire namespace as real call ids.
|
|
555
|
+
// Argument/input events can use the prefixed form, while final
|
|
556
|
+
// output_item.done events below use exact call ids; keep aliases in a
|
|
557
|
+
// separate map so a real `call_id: "fc_x"` cannot overwrite the alias
|
|
558
|
+
// for `call_id: "x"`.
|
|
559
|
+
const alias = openItemsByPrefixedCallId.get(event.item_id);
|
|
560
|
+
if (alias?.item.type === type) return alias;
|
|
561
|
+
const exact = openItemsByItemId.get(event.item_id);
|
|
562
|
+
if (exact) return exact;
|
|
563
|
+
}
|
|
564
|
+
return lookupOpenItem(event);
|
|
565
|
+
};
|
|
530
566
|
const lookupOpenFunctionCallItem = (event: {
|
|
531
567
|
output_index?: number;
|
|
532
568
|
item_id?: string;
|
|
533
569
|
}): StreamingItem | undefined => {
|
|
534
|
-
if (hasOpenItemKey(event)) return
|
|
570
|
+
if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
|
|
535
571
|
for (const candidate of openItemsInOrder) {
|
|
536
572
|
if (
|
|
537
573
|
candidate.item.type === "function_call" &&
|
|
@@ -548,10 +584,19 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
548
584
|
itemId: string | undefined,
|
|
549
585
|
entry: StreamingItem | undefined,
|
|
550
586
|
alternateItemKey?: string,
|
|
587
|
+
prefixedAlternateItemKey?: string,
|
|
551
588
|
): void => {
|
|
552
589
|
if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
|
|
553
590
|
if (itemId) openItemsByItemId.delete(itemId);
|
|
554
591
|
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.delete(alternateItemKey);
|
|
592
|
+
if (
|
|
593
|
+
prefixedAlternateItemKey &&
|
|
594
|
+
prefixedAlternateItemKey !== itemId &&
|
|
595
|
+
prefixedAlternateItemKey !== alternateItemKey &&
|
|
596
|
+
openItemsByPrefixedCallId.get(prefixedAlternateItemKey) === entry
|
|
597
|
+
) {
|
|
598
|
+
openItemsByPrefixedCallId.delete(prefixedAlternateItemKey);
|
|
599
|
+
}
|
|
555
600
|
if (entry) {
|
|
556
601
|
const index = openItemsInOrder.indexOf(entry);
|
|
557
602
|
if (index >= 0) openItemsInOrder.splice(index, 1);
|
|
@@ -591,7 +636,13 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
591
636
|
partialJson: item.arguments || "",
|
|
592
637
|
};
|
|
593
638
|
output.content.push(block);
|
|
594
|
-
registerOpenItem(
|
|
639
|
+
registerOpenItem(
|
|
640
|
+
event.output_index,
|
|
641
|
+
item.id,
|
|
642
|
+
{ item, block },
|
|
643
|
+
item.call_id,
|
|
644
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
645
|
+
);
|
|
595
646
|
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
596
647
|
} else if (item.type === "custom_tool_call") {
|
|
597
648
|
const block: StreamingToolCallBlock = {
|
|
@@ -609,7 +660,13 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
609
660
|
partialJson: item.input ?? "",
|
|
610
661
|
};
|
|
611
662
|
output.content.push(block);
|
|
612
|
-
registerOpenItem(
|
|
663
|
+
registerOpenItem(
|
|
664
|
+
event.output_index,
|
|
665
|
+
item.id,
|
|
666
|
+
{ item, block },
|
|
667
|
+
item.call_id,
|
|
668
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
669
|
+
);
|
|
613
670
|
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
614
671
|
}
|
|
615
672
|
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
@@ -739,7 +796,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
739
796
|
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
740
797
|
}
|
|
741
798
|
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
742
|
-
const entry =
|
|
799
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
743
800
|
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
744
801
|
const block = entry.block;
|
|
745
802
|
block.partialJson += event.delta;
|
|
@@ -752,7 +809,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
752
809
|
});
|
|
753
810
|
}
|
|
754
811
|
} else if (event.type === "response.custom_tool_call_input.done") {
|
|
755
|
-
const entry =
|
|
812
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
756
813
|
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
757
814
|
entry.block.partialJson = event.input;
|
|
758
815
|
entry.block.arguments = { input: event.input };
|
|
@@ -842,7 +899,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
842
899
|
output.content.push(toolCall);
|
|
843
900
|
contentIndex = output.content.length - 1;
|
|
844
901
|
}
|
|
845
|
-
closeOpenItem(event.output_index, item.id, entry, item.call_id);
|
|
902
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
846
903
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
847
904
|
} else if (item.type === "custom_tool_call") {
|
|
848
905
|
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
@@ -866,7 +923,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
866
923
|
output.content.push(toolCall);
|
|
867
924
|
contentIndex = output.content.length - 1;
|
|
868
925
|
}
|
|
869
|
-
closeOpenItem(event.output_index, item.id, entry, item.call_id);
|
|
926
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
870
927
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
871
928
|
}
|
|
872
929
|
} else if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
@@ -34,7 +34,13 @@ import {
|
|
|
34
34
|
import { postOpenAIStream } from "../utils/openai-http";
|
|
35
35
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
36
36
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
adaptSchemaForStrict,
|
|
39
|
+
findStrictToolSchemaViolation,
|
|
40
|
+
NO_STRICT,
|
|
41
|
+
sanitizeSchemaForOpenAIResponses,
|
|
42
|
+
toolWireSchema,
|
|
43
|
+
} from "../utils/schema";
|
|
38
44
|
import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
39
45
|
import {
|
|
40
46
|
buildCopilotDynamicHeaders,
|
|
@@ -398,7 +404,9 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
398
404
|
} = createRequestSetup(model, context, apiKey, options?.headers, options?.initiatorOverride, routingSessionId);
|
|
399
405
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
400
406
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
401
|
-
const
|
|
407
|
+
const builtParams = buildParams(model, context, options, providerSessionState);
|
|
408
|
+
const params = builtParams.params;
|
|
409
|
+
const { trailingScaffoldingItems } = builtParams;
|
|
402
410
|
if (isOpenAIResponsesStatefulEnabled(options, baseUrl) && routingSessionId && providerSessionState) {
|
|
403
411
|
chainState = getOpenAIResponsesChainState(providerSessionState, model, routingSessionId);
|
|
404
412
|
if (!chainState.disabled) {
|
|
@@ -406,7 +414,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
406
414
|
params.store = true;
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
|
-
|
|
417
|
+
let chained: OpenAIResponsesChainedParams =
|
|
410
418
|
chainState && !chainState.disabled
|
|
411
419
|
? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
|
|
412
420
|
: { params };
|
|
@@ -416,8 +424,14 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
416
424
|
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
417
425
|
const requestTimeoutMs =
|
|
418
426
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
419
|
-
options?.onPayload?.(params);
|
|
420
427
|
const requestUrl = `${(baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/responses`;
|
|
428
|
+
const applyPayloadReplacement = async (requestParams: OpenAIResponsesSamplingParams) => {
|
|
429
|
+
const replacementPayload = await options?.onPayload?.(requestParams, model);
|
|
430
|
+
return replacementPayload !== undefined
|
|
431
|
+
? (replacementPayload as OpenAIResponsesSamplingParams)
|
|
432
|
+
: requestParams;
|
|
433
|
+
};
|
|
434
|
+
chained = { ...chained, params: await applyPayloadReplacement(chained.params) };
|
|
421
435
|
rawRequestDump = {
|
|
422
436
|
provider: model.provider,
|
|
423
437
|
api: output.api,
|
|
@@ -492,8 +506,9 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
492
506
|
registerOpenAIResponsesChainStaleFailure(chainState, error);
|
|
493
507
|
}
|
|
494
508
|
sentPreviousResponseId = undefined;
|
|
495
|
-
|
|
496
|
-
|
|
509
|
+
const retryParams = await applyPayloadReplacement(params);
|
|
510
|
+
rawRequestDump.body = retryParams;
|
|
511
|
+
openaiStream = await openResponsesStream(retryParams);
|
|
497
512
|
}
|
|
498
513
|
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
499
514
|
stream.push({ type: "start", partial: output });
|
|
@@ -652,7 +667,8 @@ function getOpenAIResponsesRoutingSessionId(
|
|
|
652
667
|
return normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
|
|
653
668
|
}
|
|
654
669
|
|
|
655
|
-
|
|
670
|
+
/** @internal Exported for tests. */
|
|
671
|
+
export function buildParams(
|
|
656
672
|
model: Model<"openai-responses">,
|
|
657
673
|
context: Context,
|
|
658
674
|
options: OpenAIResponsesOptions | undefined,
|
|
@@ -705,7 +721,21 @@ function buildParams(
|
|
|
705
721
|
if (context.tools) {
|
|
706
722
|
params.tools = convertTools(context.tools, model.compat.supportsStrictMode, model);
|
|
707
723
|
if (options?.toolChoice) {
|
|
708
|
-
|
|
724
|
+
// Map tool_choice against the tools that survived quarantine, not the
|
|
725
|
+
// original list: a forced choice for a dropped tool — or "required" when
|
|
726
|
+
// every tool was dropped — would otherwise send a tool_choice with no
|
|
727
|
+
// matching tool, which the provider rejects just like the bad schema did (#2652).
|
|
728
|
+
const emittedNames = new Set(
|
|
729
|
+
params.tools.map(t => (t as { name?: string }).name).filter((n): n is string => n !== undefined),
|
|
730
|
+
);
|
|
731
|
+
const survivingTools =
|
|
732
|
+
params.tools.length === context.tools.length
|
|
733
|
+
? context.tools
|
|
734
|
+
: context.tools.filter(t => emittedNames.has(t.customWireName ?? t.name));
|
|
735
|
+
const toolChoice = mapOpenAIResponsesToolChoiceForTools(options.toolChoice, survivingTools, model);
|
|
736
|
+
if (toolChoice !== undefined && params.tools.length > 0) {
|
|
737
|
+
params.tool_choice = toolChoice;
|
|
738
|
+
}
|
|
709
739
|
}
|
|
710
740
|
// The apply_patch spec §1 marks only `apply_patch` itself as
|
|
711
741
|
// `supports_parallel_tool_calls = false`. OpenAI's Responses API
|
|
@@ -852,11 +882,20 @@ export function mapOpenAIResponsesToolChoiceForTools(
|
|
|
852
882
|
}
|
|
853
883
|
|
|
854
884
|
/** @internal Exported for tests. */
|
|
855
|
-
export function convertTools(
|
|
885
|
+
export function convertTools(
|
|
886
|
+
tools: Tool[],
|
|
887
|
+
strictMode: boolean,
|
|
888
|
+
model: Model<"openai-responses">,
|
|
889
|
+
onQuarantine: (toolName: string, schemaPath: string) => void = (toolName, schemaPath) =>
|
|
890
|
+
logger.warn(
|
|
891
|
+
`Tool "${toolName}" omitted from the openai-responses request: its parameter schema is invalid for this provider at ${schemaPath} (an enum/const value cannot match its declared type). Other tools are unaffected.`,
|
|
892
|
+
),
|
|
893
|
+
): OpenAITool[] {
|
|
856
894
|
const allowFreeform = supportsFreeformApplyPatch(model);
|
|
857
|
-
|
|
895
|
+
const out: OpenAITool[] = [];
|
|
896
|
+
for (const tool of tools) {
|
|
858
897
|
if (allowFreeform && tool.customFormat) {
|
|
859
|
-
|
|
898
|
+
out.push({
|
|
860
899
|
type: "custom",
|
|
861
900
|
// Tool advertises its wire-level name (e.g. `apply_patch`) — the
|
|
862
901
|
// agent-loop dispatcher will match incoming calls by either the
|
|
@@ -868,18 +907,29 @@ export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"o
|
|
|
868
907
|
syntax: tool.customFormat.syntax,
|
|
869
908
|
definition: compactGrammarDefinition(tool.customFormat.syntax, tool.customFormat.definition),
|
|
870
909
|
},
|
|
871
|
-
} as unknown as OpenAITool;
|
|
910
|
+
} as unknown as OpenAITool);
|
|
911
|
+
continue;
|
|
872
912
|
}
|
|
873
913
|
const strict = !NO_STRICT && strictMode && tool.strict !== false;
|
|
874
914
|
const baseParameters = toolWireSchema(tool);
|
|
875
915
|
const responseParameters = sanitizeSchemaForOpenAIResponses(baseParameters);
|
|
876
916
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
|
|
877
|
-
|
|
917
|
+
// Quarantine a tool whose emitted schema carries a provider-rejecting
|
|
918
|
+
// enum/const-vs-type contradiction: dropping just that tool keeps the rest
|
|
919
|
+
// of the request valid instead of letting one bad MCP schema 400 the whole
|
|
920
|
+
// turn (#2652). Other tools and built-ins are unaffected.
|
|
921
|
+
const violation = findStrictToolSchemaViolation(parameters);
|
|
922
|
+
if (violation) {
|
|
923
|
+
onQuarantine(tool.name, violation);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
out.push({
|
|
878
927
|
type: "function",
|
|
879
928
|
name: tool.name,
|
|
880
929
|
description: tool.description || "",
|
|
881
930
|
parameters,
|
|
882
931
|
...(effectiveStrict && { strict: true }),
|
|
883
|
-
} as OpenAITool;
|
|
884
|
-
}
|
|
932
|
+
} as OpenAITool);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
885
935
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Codex (ChatGPT OAuth) flow — browser and device-code flows.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex";
|
|
4
6
|
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
|
|
5
7
|
import { generatePKCE } from "./pkce";
|
|
6
8
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
@@ -60,6 +62,29 @@ interface PKCE {
|
|
|
60
62
|
verifier: string;
|
|
61
63
|
challenge: string;
|
|
62
64
|
}
|
|
65
|
+
/** Builds the Codex browser OAuth URL used by browser login; exported for auth regression tests. */
|
|
66
|
+
export function createOpenAICodexAuthorizationUrl(args: {
|
|
67
|
+
state: string;
|
|
68
|
+
redirectUri: string;
|
|
69
|
+
challenge: string;
|
|
70
|
+
originator?: string;
|
|
71
|
+
}): string {
|
|
72
|
+
const originator = args.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
73
|
+
const searchParams = new URLSearchParams({
|
|
74
|
+
response_type: "code",
|
|
75
|
+
client_id: CLIENT_ID,
|
|
76
|
+
redirect_uri: args.redirectUri,
|
|
77
|
+
scope: SCOPE,
|
|
78
|
+
code_challenge: args.challenge,
|
|
79
|
+
code_challenge_method: "S256",
|
|
80
|
+
state: args.state,
|
|
81
|
+
id_token_add_organizations: "true",
|
|
82
|
+
codex_cli_simplified_flow: "true",
|
|
83
|
+
originator,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return `${AUTHORIZE_URL}?${searchParams.toString()}`;
|
|
87
|
+
}
|
|
63
88
|
|
|
64
89
|
class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
65
90
|
constructor(
|
|
@@ -79,20 +104,12 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
82
|
-
const
|
|
83
|
-
response_type: "code",
|
|
84
|
-
client_id: CLIENT_ID,
|
|
85
|
-
redirect_uri: redirectUri,
|
|
86
|
-
scope: SCOPE,
|
|
87
|
-
code_challenge: this.pkce.challenge,
|
|
88
|
-
code_challenge_method: "S256",
|
|
107
|
+
const url = createOpenAICodexAuthorizationUrl({
|
|
89
108
|
state,
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
redirectUri,
|
|
110
|
+
challenge: this.pkce.challenge,
|
|
92
111
|
originator: this.originator,
|
|
93
112
|
});
|
|
94
|
-
|
|
95
|
-
const url = `${AUTHORIZE_URL}?${searchParams.toString()}`;
|
|
96
113
|
return { url, instructions: "A browser window should open. Complete login to finish." };
|
|
97
114
|
}
|
|
98
115
|
|
|
@@ -153,13 +170,13 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri:
|
|
|
153
170
|
* Login with OpenAI Codex OAuth
|
|
154
171
|
*/
|
|
155
172
|
export type OpenAICodexLoginOptions = OAuthController & {
|
|
156
|
-
/** Optional originator value for OpenAI Codex OAuth. Default
|
|
173
|
+
/** Optional originator value for OpenAI Codex OAuth. Default matches OMP Codex request headers. */
|
|
157
174
|
originator?: string;
|
|
158
175
|
};
|
|
159
176
|
|
|
160
177
|
export async function loginOpenAICodex(options: OpenAICodexLoginOptions): Promise<OAuthCredentials> {
|
|
161
178
|
const pkce = await generatePKCE();
|
|
162
|
-
const originator = options.originator?.trim() ||
|
|
179
|
+
const originator = options.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
163
180
|
const flow = new OpenAICodexOAuthFlow(options, pkce, originator);
|
|
164
181
|
|
|
165
182
|
return flow.login();
|
package/src/registry/registry.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { syntheticProvider } from "./synthetic";
|
|
|
46
46
|
import { tavilyProvider } from "./tavily";
|
|
47
47
|
import { togetherProvider } from "./together";
|
|
48
48
|
import type { ProviderDefinition } from "./types";
|
|
49
|
+
import { umansProvider } from "./umans";
|
|
49
50
|
import { veniceProvider } from "./venice";
|
|
50
51
|
import { vercelAiGatewayProvider } from "./vercel-ai-gateway";
|
|
51
52
|
import { vllmProvider } from "./vllm";
|
|
@@ -85,6 +86,7 @@ const ALL = [
|
|
|
85
86
|
alibabaCodingPlanProvider,
|
|
86
87
|
aimlApiProvider,
|
|
87
88
|
zhipuCodingPlanProvider,
|
|
89
|
+
umansProvider,
|
|
88
90
|
qwenPortalProvider,
|
|
89
91
|
minimaxCodeProvider,
|
|
90
92
|
minimaxCodeCnProvider,
|