@oh-my-pi/pi-ai 17.0.4 → 17.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- 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/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 +84 -18
- 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/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/utils/schema/normalize.ts +103 -11
- package/src/utils.ts +10 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scheduler } from "node:timers/promises";
|
|
1
2
|
import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
2
3
|
import { $flag, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import * as AIError from "../error";
|
|
@@ -155,6 +156,33 @@ const OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE =
|
|
|
155
156
|
"OpenAI responses stream timed out while waiting for the first event";
|
|
156
157
|
/** Consecutive stale-previous-response failures before chaining is disabled for the session. */
|
|
157
158
|
const OPENAI_RESPONSES_CHAIN_STALE_FAILURE_LIMIT = 3;
|
|
159
|
+
const OPENAI_RESPONSES_MAX_TRANSIENT_STREAM_RETRIES = 1;
|
|
160
|
+
const OPENAI_RESPONSES_TRANSIENT_STREAM_RETRY_DELAY_MS = 500;
|
|
161
|
+
|
|
162
|
+
function isOpenAIResponsesReplayUnsafeEvent(event: ResponseStreamEvent): boolean {
|
|
163
|
+
switch (event.type) {
|
|
164
|
+
case "response.output_text.delta":
|
|
165
|
+
case "response.refusal.delta":
|
|
166
|
+
case "response.reasoning_summary_text.delta":
|
|
167
|
+
case "response.reasoning_text.delta":
|
|
168
|
+
case "response.function_call_arguments.delta":
|
|
169
|
+
case "response.custom_tool_call_input.delta":
|
|
170
|
+
return typeof event.delta === "string" && event.delta.length > 0;
|
|
171
|
+
case "response.reasoning_summary_part.done":
|
|
172
|
+
return true;
|
|
173
|
+
case "response.output_item.done":
|
|
174
|
+
return true;
|
|
175
|
+
default:
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRetryableOpenAIResponsesStreamFailure(error: unknown): boolean {
|
|
181
|
+
return (
|
|
182
|
+
AIError.isTransientStreamParseError(error) ||
|
|
183
|
+
(error instanceof AIError.ProviderResponseError && error.kind === "incomplete-stream")
|
|
184
|
+
);
|
|
185
|
+
}
|
|
158
186
|
|
|
159
187
|
interface OpenAIResponsesProviderSessionState
|
|
160
188
|
extends ProviderSessionState,
|
|
@@ -453,7 +481,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
453
481
|
return payload;
|
|
454
482
|
};
|
|
455
483
|
chained = { ...chained, params: await applyPayloadReplacement(chained.params) };
|
|
456
|
-
|
|
484
|
+
const activeRawRequestDump: RawHttpRequestDump = {
|
|
457
485
|
provider: model.provider,
|
|
458
486
|
api: output.api,
|
|
459
487
|
model: model.id,
|
|
@@ -461,6 +489,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
461
489
|
url: requestUrl,
|
|
462
490
|
body: chained.params,
|
|
463
491
|
};
|
|
492
|
+
rawRequestDump = activeRawRequestDump;
|
|
464
493
|
const openResponsesStream = (requestParams: OpenAIResponsesSamplingParams) => {
|
|
465
494
|
activeReasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
466
495
|
"responses",
|
|
@@ -508,187 +537,257 @@ const streamOpenAIResponsesOnce = (
|
|
|
508
537
|
{ provider: model.provider, signal: requestSignal },
|
|
509
538
|
);
|
|
510
539
|
};
|
|
511
|
-
let openaiStream: AsyncIterable<ResponseStreamEvent>;
|
|
512
540
|
let strictRetryAvailable = true;
|
|
513
541
|
let activeStrictToolsApplied = builtParams.strictToolsApplied;
|
|
514
542
|
let forceDisableStrictTools = false;
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
543
|
+
const openResponsesStreamWithFallbacks = async (): Promise<AsyncIterable<ResponseStreamEvent>> => {
|
|
544
|
+
let openaiStream: AsyncIterable<ResponseStreamEvent>;
|
|
545
|
+
while (true) {
|
|
546
|
+
try {
|
|
547
|
+
openaiStream = await openResponsesStream(chained.params);
|
|
548
|
+
if (pendingReasoningEffortFallback) {
|
|
549
|
+
rememberOpenAIReasoningEffortFallback(
|
|
550
|
+
providerSessionState,
|
|
551
|
+
pendingReasoningEffortFallback.key,
|
|
552
|
+
pendingReasoningEffortFallback.fallback,
|
|
553
|
+
);
|
|
554
|
+
pendingReasoningEffortFallback = undefined;
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
559
|
+
const reasoningEffortFallback =
|
|
560
|
+
activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
|
|
561
|
+
? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
|
|
562
|
+
explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
|
|
563
|
+
})
|
|
564
|
+
: undefined;
|
|
565
|
+
if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
|
|
566
|
+
const retryMarker = `${activeReasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
|
|
567
|
+
if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
|
|
568
|
+
attemptedReasoningEffortFallbacks.add(retryMarker);
|
|
569
|
+
requestReasoningEffortFallbacks.set(activeReasoningEffortFallbackKey, reasoningEffortFallback);
|
|
570
|
+
applyOpenAIReasoningEffortFallback(chained.params, reasoningEffortFallback);
|
|
571
|
+
applyOpenAIReasoningEffortFallback(activeParams, reasoningEffortFallback);
|
|
572
|
+
activeRawRequestDump.body = chained.params;
|
|
573
|
+
pendingReasoningEffortFallback = {
|
|
574
|
+
key: activeReasoningEffortFallbackKey,
|
|
575
|
+
fallback: reasoningEffortFallback,
|
|
576
|
+
};
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const compiledGrammarTooLarge =
|
|
580
|
+
isOpenRouterAnthropicModel(model) &&
|
|
581
|
+
isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse);
|
|
582
|
+
const canRetryWithoutStrictTools =
|
|
583
|
+
strictRetryAvailable &&
|
|
584
|
+
!requestSignal.aborted &&
|
|
585
|
+
(compiledGrammarTooLarge ||
|
|
586
|
+
shouldRetryWithoutStrictTools(
|
|
587
|
+
error,
|
|
588
|
+
capturedErrorResponse,
|
|
589
|
+
activeStrictToolsApplied,
|
|
590
|
+
context.tools,
|
|
591
|
+
));
|
|
592
|
+
if (canRetryWithoutStrictTools) {
|
|
593
|
+
strictRetryAvailable = false;
|
|
594
|
+
forceDisableStrictTools = true;
|
|
595
|
+
disableStrictToolsForScope(providerSessionState, strictToolsScope);
|
|
596
|
+
const fallbackBuilt = buildParams(
|
|
597
|
+
model,
|
|
598
|
+
context,
|
|
599
|
+
options,
|
|
600
|
+
providerSessionState,
|
|
601
|
+
strictToolsScope,
|
|
602
|
+
true,
|
|
603
|
+
);
|
|
604
|
+
const fallbackParams = fallbackBuilt.params;
|
|
605
|
+
if (chainState && !chainState.disabled) fallbackParams.store = true;
|
|
606
|
+
let fallbackChained: OpenAIResponsesChainedParams =
|
|
607
|
+
chainState && !chainState.disabled
|
|
608
|
+
? buildOpenAIResponsesChainedParams(fallbackParams, chainState)
|
|
609
|
+
: { params: fallbackParams };
|
|
610
|
+
sentPreviousResponseId = fallbackChained.previousResponseId;
|
|
611
|
+
fallbackChained = {
|
|
612
|
+
...fallbackChained,
|
|
613
|
+
params: await applyPayloadReplacement(fallbackChained.params),
|
|
614
|
+
};
|
|
615
|
+
chained = fallbackChained;
|
|
616
|
+
activeRawRequestDump.body = chained.params;
|
|
617
|
+
activeParams = fallbackParams;
|
|
618
|
+
activeStrictToolsApplied = fallbackBuilt.strictToolsApplied;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (!chainState || !sentPreviousResponseId || requestSignal.aborted) {
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
const zdrRejection =
|
|
625
|
+
error instanceof Error &&
|
|
626
|
+
/previous[ _]?response/i.test(error.message) &&
|
|
627
|
+
/zero[ _-]?data[ _-]?retention/i.test(error.message);
|
|
628
|
+
const isPromptBlocked =
|
|
629
|
+
error instanceof Error &&
|
|
630
|
+
((error as { code?: string }).code === "invalid_prompt" ||
|
|
631
|
+
/invalid_prompt|Request blocked/i.test(error.message));
|
|
632
|
+
if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
// Server rejected the chain baseline: reset, count the failure (or
|
|
636
|
+
// disable categorically on ZDR), and retry once with the full
|
|
637
|
+
// transcript. Structurally cannot loop — the retry carries no
|
|
638
|
+
// previous_response_id.
|
|
639
|
+
if (zdrRejection) {
|
|
640
|
+
markOpenAIResponsesChainZeroDataRetention(chainState, error);
|
|
641
|
+
// ZDR orgs cannot store responses; the retry uses `store: false`.
|
|
642
|
+
} else {
|
|
643
|
+
registerOpenAIResponsesChainStaleFailure(chainState, error);
|
|
644
|
+
}
|
|
645
|
+
sentPreviousResponseId = undefined;
|
|
646
|
+
const currentBuilt = buildParams(
|
|
567
647
|
model,
|
|
568
648
|
context,
|
|
569
649
|
options,
|
|
570
650
|
providerSessionState,
|
|
571
651
|
strictToolsScope,
|
|
572
|
-
|
|
652
|
+
forceDisableStrictTools,
|
|
573
653
|
);
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
chained = fallbackChained;
|
|
586
|
-
rawRequestDump.body = chained.params;
|
|
587
|
-
activeParams = fallbackParams;
|
|
588
|
-
activeStrictToolsApplied = fallbackBuilt.strictToolsApplied;
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
if (!chainState || !sentPreviousResponseId || requestSignal.aborted) {
|
|
592
|
-
throw error;
|
|
654
|
+
const currentParams = currentBuilt.params;
|
|
655
|
+
// Only ZDR forces `store: false` (the org never persists responses). A
|
|
656
|
+
// non-ZDR stale baseline is transient, so keep storing: the full-context
|
|
657
|
+
// retry must be chainable next turn, and the consecutive stale-failure
|
|
658
|
+
// breaker only trips when each retry stores and the next turn re-chains.
|
|
659
|
+
currentParams.store = !zdrRejection;
|
|
660
|
+
const retryParams = await applyPayloadReplacement(currentParams);
|
|
661
|
+
chained = { params: retryParams };
|
|
662
|
+
activeRawRequestDump.body = retryParams;
|
|
663
|
+
activeParams = currentParams;
|
|
664
|
+
activeStrictToolsApplied = currentBuilt.strictToolsApplied;
|
|
593
665
|
}
|
|
594
|
-
const zdrRejection =
|
|
595
|
-
error instanceof Error &&
|
|
596
|
-
/previous[ _]?response/i.test(error.message) &&
|
|
597
|
-
/zero[ _-]?data[ _-]?retention/i.test(error.message);
|
|
598
|
-
const isPromptBlocked =
|
|
599
|
-
error instanceof Error &&
|
|
600
|
-
((error as { code?: string }).code === "invalid_prompt" ||
|
|
601
|
-
/invalid_prompt|Request blocked/i.test(error.message));
|
|
602
|
-
if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
605
|
-
// Server rejected the chain baseline: reset, count the failure (or
|
|
606
|
-
// disable categorically on ZDR), and retry once with the full
|
|
607
|
-
// transcript. Structurally cannot loop — the retry carries no
|
|
608
|
-
// previous_response_id.
|
|
609
|
-
if (zdrRejection) {
|
|
610
|
-
markOpenAIResponsesChainZeroDataRetention(chainState, error);
|
|
611
|
-
// ZDR orgs cannot store responses; the retry uses `store: false`.
|
|
612
|
-
} else {
|
|
613
|
-
registerOpenAIResponsesChainStaleFailure(chainState, error);
|
|
614
|
-
}
|
|
615
|
-
sentPreviousResponseId = undefined;
|
|
616
|
-
const currentBuilt = buildParams(
|
|
617
|
-
model,
|
|
618
|
-
context,
|
|
619
|
-
options,
|
|
620
|
-
providerSessionState,
|
|
621
|
-
strictToolsScope,
|
|
622
|
-
forceDisableStrictTools,
|
|
623
|
-
);
|
|
624
|
-
const currentParams = currentBuilt.params;
|
|
625
|
-
// Only ZDR forces `store: false` (the org never persists responses). A
|
|
626
|
-
// non-ZDR stale baseline is transient, so keep storing: the full-context
|
|
627
|
-
// retry must be chainable next turn, and the consecutive stale-failure
|
|
628
|
-
// breaker only trips when each retry stores and the next turn re-chains.
|
|
629
|
-
currentParams.store = !zdrRejection;
|
|
630
|
-
const retryParams = await applyPayloadReplacement(currentParams);
|
|
631
|
-
chained = { params: retryParams };
|
|
632
|
-
rawRequestDump.body = retryParams;
|
|
633
|
-
activeParams = currentParams;
|
|
634
|
-
activeStrictToolsApplied = currentBuilt.strictToolsApplied;
|
|
635
666
|
}
|
|
636
|
-
|
|
667
|
+
return openaiStream;
|
|
668
|
+
};
|
|
669
|
+
let openaiStream = await openResponsesStreamWithFallbacks();
|
|
637
670
|
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
638
671
|
stream.push({ type: "start", partial: output });
|
|
639
672
|
|
|
640
673
|
const nativeOutputItems: Array<Record<string, unknown>> = [];
|
|
641
|
-
let
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
674
|
+
let transientStreamRetryAttempt = 0;
|
|
675
|
+
while (true) {
|
|
676
|
+
let sawReplayUnsafeOutput = false;
|
|
677
|
+
let sawTerminalResponseEvent = false;
|
|
678
|
+
const attemptStream = new AssistantMessageEventStream();
|
|
679
|
+
let forwardAttemptLive = false;
|
|
680
|
+
const forwardAttemptEvents = () => {
|
|
681
|
+
for (const event of attemptStream.queue) stream.push(event);
|
|
682
|
+
attemptStream.queue.length = 0;
|
|
683
|
+
};
|
|
684
|
+
nativeOutputItems.length = 0;
|
|
685
|
+
const timedOpenaiStream = iterateWithIdleTimeout(openaiStream, {
|
|
686
|
+
idleTimeoutMs,
|
|
687
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
688
|
+
firstItemErrorMessage: OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
689
|
+
errorMessage: "OpenAI responses stream stalled while waiting for the next event",
|
|
690
|
+
onFirstItemTimeout: () => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
691
|
+
onIdle: () => requestAbortController.abort(),
|
|
692
|
+
abortSignal: options?.signal,
|
|
693
|
+
isProgressItem: isOpenAIResponsesProgressEvent,
|
|
694
|
+
});
|
|
695
|
+
const observedOpenaiStream = (async function* (): AsyncGenerator<ResponseStreamEvent> {
|
|
696
|
+
for await (const event of timedOpenaiStream) {
|
|
697
|
+
if (isOpenAIResponsesReplayUnsafeEvent(event)) {
|
|
698
|
+
sawReplayUnsafeOutput = true;
|
|
699
|
+
if (!forwardAttemptLive) {
|
|
700
|
+
forwardAttemptEvents();
|
|
701
|
+
forwardAttemptLive = true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
yield event;
|
|
705
|
+
if (forwardAttemptLive) forwardAttemptEvents();
|
|
706
|
+
}
|
|
707
|
+
})();
|
|
666
708
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
709
|
+
try {
|
|
710
|
+
await processResponsesStream(observedOpenaiStream, output, attemptStream, model, {
|
|
711
|
+
onFirstToken: () => {
|
|
712
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
713
|
+
},
|
|
714
|
+
onOutputItemDone: item => {
|
|
715
|
+
// `processResponsesStream` hands over a private clone already; no
|
|
716
|
+
// second deep copy needed (reasoning items carry multi-KB blobs).
|
|
717
|
+
nativeOutputItems.push(item as unknown as Record<string, unknown>);
|
|
718
|
+
},
|
|
719
|
+
onCompleted: () => {
|
|
720
|
+
sawTerminalResponseEvent = true;
|
|
721
|
+
},
|
|
722
|
+
requestServiceTier: options?.serviceTier,
|
|
723
|
+
});
|
|
674
724
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// this guard the incomplete output is silently surfaced as a successful
|
|
679
|
-
// "stop".
|
|
680
|
-
if (!sawTerminalResponseEvent) {
|
|
681
|
-
throw new AIError.ProviderResponseError(
|
|
682
|
-
"OpenAI responses stream closed before a terminal response event was received",
|
|
683
|
-
{ provider: model.provider, kind: "incomplete-stream" },
|
|
684
|
-
);
|
|
685
|
-
}
|
|
725
|
+
const localAbortReason = abortTracker.getLocalAbortReason();
|
|
726
|
+
if (localAbortReason) throw localAbortReason;
|
|
727
|
+
if (abortTracker.wasCallerAbort()) throw new AIError.AbortError();
|
|
686
728
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
729
|
+
// Detect premature stream closure: the HTTP stream ended without the
|
|
730
|
+
// provider sending a recognized terminal response event.
|
|
731
|
+
if (!sawTerminalResponseEvent) {
|
|
732
|
+
throw new AIError.ProviderResponseError(
|
|
733
|
+
"OpenAI responses stream closed before a terminal response event was received",
|
|
734
|
+
{ provider: model.provider, kind: "incomplete-stream" },
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
739
|
+
throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
|
|
740
|
+
provider: model.provider,
|
|
741
|
+
kind: "runtime",
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
forwardAttemptEvents();
|
|
745
|
+
break;
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const streamFailure = abortTracker.getLocalAbortReason() ?? error;
|
|
748
|
+
const canRetry =
|
|
749
|
+
!sawReplayUnsafeOutput &&
|
|
750
|
+
!requestSignal.aborted &&
|
|
751
|
+
!abortTracker.wasCallerAbort() &&
|
|
752
|
+
transientStreamRetryAttempt < OPENAI_RESPONSES_MAX_TRANSIENT_STREAM_RETRIES &&
|
|
753
|
+
isRetryableOpenAIResponsesStreamFailure(streamFailure);
|
|
754
|
+
if (!canRetry) {
|
|
755
|
+
forwardAttemptEvents();
|
|
756
|
+
throw streamFailure;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
transientStreamRetryAttempt++;
|
|
760
|
+
logger.debug("OpenAI responses stream ended before replay-unsafe output; retrying", {
|
|
761
|
+
provider: model.provider,
|
|
762
|
+
model: model.id,
|
|
763
|
+
attempt: transientStreamRetryAttempt,
|
|
764
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
765
|
+
});
|
|
766
|
+
const retryOutput = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
|
|
767
|
+
output.content.length = 0;
|
|
768
|
+
output.responseId = undefined;
|
|
769
|
+
output.upstreamProvider = undefined;
|
|
770
|
+
output.errorMessage = undefined;
|
|
771
|
+
output.errorStatus = undefined;
|
|
772
|
+
output.errorId = undefined;
|
|
773
|
+
output.stopDetails = undefined;
|
|
774
|
+
output.providerPayload = undefined;
|
|
775
|
+
output.usage = retryOutput.usage;
|
|
776
|
+
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
777
|
+
output.stopReason = "stop";
|
|
778
|
+
output.duration = undefined;
|
|
779
|
+
output.ttft = undefined;
|
|
780
|
+
firstTokenTime = undefined;
|
|
781
|
+
nativeOutputItems.length = 0;
|
|
782
|
+
|
|
783
|
+
if (options?.providerRetryWait) {
|
|
784
|
+
await options.providerRetryWait(OPENAI_RESPONSES_TRANSIENT_STREAM_RETRY_DELAY_MS, options.signal);
|
|
785
|
+
} else {
|
|
786
|
+
await scheduler.wait(OPENAI_RESPONSES_TRANSIENT_STREAM_RETRY_DELAY_MS, { signal: options?.signal });
|
|
787
|
+
}
|
|
788
|
+
if (abortTracker.wasCallerAbort()) throw new AIError.AbortError();
|
|
789
|
+
openaiStream = await openResponsesStreamWithFallbacks();
|
|
790
|
+
}
|
|
692
791
|
}
|
|
693
792
|
|
|
694
793
|
output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
|
|
@@ -31,7 +31,7 @@ type AnthropicMessagesValidation = {
|
|
|
31
31
|
type ModelsEndpointValidation = {
|
|
32
32
|
kind: "models-endpoint";
|
|
33
33
|
provider: string;
|
|
34
|
-
modelsUrl: string;
|
|
34
|
+
modelsUrl: string | (() => string);
|
|
35
35
|
headers?: Record<string, string> | (() => Record<string, string> | undefined);
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -99,7 +99,10 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon
|
|
|
99
99
|
await validateApiKeyAgainstModelsEndpoint({
|
|
100
100
|
provider: config.validation.provider,
|
|
101
101
|
apiKey: trimmed,
|
|
102
|
-
modelsUrl:
|
|
102
|
+
modelsUrl:
|
|
103
|
+
typeof config.validation.modelsUrl === "function"
|
|
104
|
+
? config.validation.modelsUrl()
|
|
105
|
+
: config.validation.modelsUrl,
|
|
103
106
|
headers: config.validation.headers,
|
|
104
107
|
signal: options.signal,
|
|
105
108
|
fetch: options.fetch,
|
package/src/registry/moonshot.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import { createApiKeyLogin } from "./api-key-login";
|
|
2
3
|
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
4
|
import type { ProviderDefinition } from "./types";
|
|
4
5
|
|
|
6
|
+
function resolveMoonshotModelsUrl(): string {
|
|
7
|
+
const baseUrl = $env.MOONSHOT_BASE_URL?.trim() || "https://api.moonshot.ai/v1";
|
|
8
|
+
return `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export const loginMoonshot = createApiKeyLogin({
|
|
6
12
|
providerLabel: "Moonshot",
|
|
7
13
|
authUrl: "https://platform.moonshot.ai/console/api-keys",
|
|
@@ -11,7 +17,7 @@ export const loginMoonshot = createApiKeyLogin({
|
|
|
11
17
|
validation: {
|
|
12
18
|
kind: "models-endpoint",
|
|
13
19
|
provider: "moonshot",
|
|
14
|
-
modelsUrl:
|
|
20
|
+
modelsUrl: resolveMoonshotModelsUrl,
|
|
15
21
|
},
|
|
16
22
|
});
|
|
17
23
|
|
|
@@ -1133,18 +1133,19 @@ const OLLAMA_SCHEMA_VALUE_KEYS = new Set([
|
|
|
1133
1133
|
]);
|
|
1134
1134
|
|
|
1135
1135
|
/**
|
|
1136
|
-
* Widened stand-in for a `true` / `{}` subschema
|
|
1136
|
+
* Widened stand-in for a `true` / `{}` open subschema on a tool bound for a
|
|
1137
|
+
* backend whose wire cannot encode a bare boolean subschema.
|
|
1137
1138
|
*
|
|
1138
1139
|
* `toolWireSchema()` normalizes empty schemas to boolean `true` upstream so
|
|
1139
|
-
* grammar-constrained samplers
|
|
1140
|
-
*
|
|
1141
|
-
* unmarshal
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
* union rather than a closed empty object.
|
|
1140
|
+
* grammar-constrained samplers don't treat `{}` as "generate an empty object"
|
|
1141
|
+
* (issue #1179). Two backends then choke on the bare boolean: Ollama's Go tool
|
|
1142
|
+
* parser can't unmarshal it into its object-shaped `Schema` struct, and
|
|
1143
|
+
* llama.cpp's JSON-schema→GBNF converter has no case for a boolean schema
|
|
1144
|
+
* (issue #5914). Both sanitizers replace the open subschema with an explicit
|
|
1145
|
+
* union of every primitive JSON type — the wire has no boolean subschema, and
|
|
1146
|
+
* a grammar sampler sees a real value union rather than a closed empty object.
|
|
1146
1147
|
*/
|
|
1147
|
-
const
|
|
1148
|
+
const OPEN_SUBSCHEMA_WIDENING = Object.freeze({
|
|
1148
1149
|
anyOf: [
|
|
1149
1150
|
{ type: "string" },
|
|
1150
1151
|
{ type: "number" },
|
|
@@ -1161,8 +1162,8 @@ const OLLAMA_OPEN_SUBSCHEMA_WIDENING = Object.freeze({
|
|
|
1161
1162
|
*/
|
|
1162
1163
|
export function sanitizeSchemaForOllama(schema: JsonObject): JsonObject {
|
|
1163
1164
|
const normalizeNode = (value: unknown): unknown => {
|
|
1164
|
-
if (value === true) return
|
|
1165
|
-
if (value === false) return { not:
|
|
1165
|
+
if (value === true) return OPEN_SUBSCHEMA_WIDENING;
|
|
1166
|
+
if (value === false) return { not: OPEN_SUBSCHEMA_WIDENING };
|
|
1166
1167
|
if (!isJsonObject(value)) {
|
|
1167
1168
|
if (!Array.isArray(value)) return value;
|
|
1168
1169
|
let changed = false;
|
|
@@ -1235,6 +1236,97 @@ export function sanitizeSchemaForOllama(schema: JsonObject): JsonObject {
|
|
|
1235
1236
|
return normalizeNode(schema) as JsonObject;
|
|
1236
1237
|
}
|
|
1237
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Schema-valued keywords whose bare boolean value must be widened for a
|
|
1241
|
+
* grammar-constrained backend. Excludes `additionalProperties` and
|
|
1242
|
+
* `unevaluatedProperties`: llama.cpp's `_build_object_rule` reads their boolean
|
|
1243
|
+
* form as meaningful closed/open-object semantics, and `additionalProperties:
|
|
1244
|
+
* false` is exactly what `toolWireSchema` emits to pin a strict object shape.
|
|
1245
|
+
*/
|
|
1246
|
+
const GRAMMAR_SCHEMA_VALUE_KEYS: Record<string, true> = {
|
|
1247
|
+
items: true,
|
|
1248
|
+
additionalItems: true,
|
|
1249
|
+
contains: true,
|
|
1250
|
+
contentSchema: true,
|
|
1251
|
+
propertyNames: true,
|
|
1252
|
+
if: true,
|
|
1253
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema keyword
|
|
1254
|
+
then: true,
|
|
1255
|
+
else: true,
|
|
1256
|
+
not: true,
|
|
1257
|
+
unevaluatedItems: true,
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Rewrites the one JSON Schema form that grammar-constrained OpenAI-compatible
|
|
1262
|
+
* backends (llama.cpp, LM Studio, vLLM) cannot compile to GBNF: a bare boolean
|
|
1263
|
+
* subschema. `toolWireSchema` normalizes `{}` open subschemas to boolean `true`
|
|
1264
|
+
* (issue #1179); llama.cpp's `json-schema-to-grammar.cpp` `visit()` has no case
|
|
1265
|
+
* for a boolean schema and throws `Unrecognized schema: true` → HTTP 400 before
|
|
1266
|
+
* the model is consulted (issue #5914).
|
|
1267
|
+
*
|
|
1268
|
+
* Narrower than {@link sanitizeSchemaForOllama}: only genuine subschema slots
|
|
1269
|
+
* are widened. Boolean `additionalProperties`/`unevaluatedProperties` stay
|
|
1270
|
+
* intact because the converter reads those as closed/open-object grammar
|
|
1271
|
+
* semantics, and dropping `additionalProperties: false` would silently reopen
|
|
1272
|
+
* every declared object.
|
|
1273
|
+
*/
|
|
1274
|
+
export function sanitizeSchemaForGrammar(schema: JsonObject): JsonObject {
|
|
1275
|
+
const normalizeNode = (value: unknown, isSubschema: boolean): unknown => {
|
|
1276
|
+
if (value === true) return isSubschema ? OPEN_SUBSCHEMA_WIDENING : value;
|
|
1277
|
+
if (value === false) return isSubschema ? { not: OPEN_SUBSCHEMA_WIDENING } : value;
|
|
1278
|
+
if (Array.isArray(value)) {
|
|
1279
|
+
let changed = false;
|
|
1280
|
+
const output = value.map(item => {
|
|
1281
|
+
const next = normalizeNode(item, isSubschema);
|
|
1282
|
+
if (next !== item) changed = true;
|
|
1283
|
+
return next;
|
|
1284
|
+
});
|
|
1285
|
+
return changed ? output : value;
|
|
1286
|
+
}
|
|
1287
|
+
if (!isJsonObject(value)) return value;
|
|
1288
|
+
|
|
1289
|
+
let changed = false;
|
|
1290
|
+
const output: JsonObject = {};
|
|
1291
|
+
for (const key in value) {
|
|
1292
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
1293
|
+
const child = value[key];
|
|
1294
|
+
let next = child;
|
|
1295
|
+
if (Object.hasOwn(SUBSCHEMA_MAP_KEYS, key) && isJsonObject(child)) {
|
|
1296
|
+
let mapChanged = false;
|
|
1297
|
+
const mapOutput: JsonObject = {};
|
|
1298
|
+
for (const childKey in child) {
|
|
1299
|
+
if (!Object.hasOwn(child, childKey)) continue;
|
|
1300
|
+
const mapChild = child[childKey];
|
|
1301
|
+
const normalizedChild = normalizeNode(mapChild, true);
|
|
1302
|
+
if (normalizedChild !== mapChild) mapChanged = true;
|
|
1303
|
+
mapOutput[childKey] = normalizedChild;
|
|
1304
|
+
}
|
|
1305
|
+
next = mapChanged ? mapOutput : child;
|
|
1306
|
+
} else if (Object.hasOwn(SUBSCHEMA_ARRAY_KEYS, key) && Array.isArray(child)) {
|
|
1307
|
+
let arrayChanged = false;
|
|
1308
|
+
const arrayOutput = child.map(item => {
|
|
1309
|
+
const normalizedItem = normalizeNode(item, true);
|
|
1310
|
+
if (normalizedItem !== item) arrayChanged = true;
|
|
1311
|
+
return normalizedItem;
|
|
1312
|
+
});
|
|
1313
|
+
next = arrayChanged ? arrayOutput : child;
|
|
1314
|
+
} else if (Object.hasOwn(GRAMMAR_SCHEMA_VALUE_KEYS, key)) {
|
|
1315
|
+
next = normalizeNode(child, true);
|
|
1316
|
+
} else if ((key === "additionalProperties" || key === "unevaluatedProperties") && typeof child !== "boolean") {
|
|
1317
|
+
// Boolean form is meaningful closed/open-object grammar semantics and
|
|
1318
|
+
// stays intact; the object form is a genuine subschema whose interior
|
|
1319
|
+
// may still hold bare booleans emitted by `toolWireSchema`.
|
|
1320
|
+
next = normalizeNode(child, true);
|
|
1321
|
+
}
|
|
1322
|
+
if (next !== child) changed = true;
|
|
1323
|
+
output[key] = next;
|
|
1324
|
+
}
|
|
1325
|
+
return changed ? output : value;
|
|
1326
|
+
};
|
|
1327
|
+
return normalizeNode(schema, true) as JsonObject;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1238
1330
|
// ---------------------------------------------------------------------------
|
|
1239
1331
|
// OpenAI Responses — schema-valued normalization
|
|
1240
1332
|
// ---------------------------------------------------------------------------
|