@oh-my-pi/pi-ai 17.0.3 → 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 +25 -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 +23 -2
- 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 +273 -165
- package/src/registry/api-key-login.ts +5 -2
- package/src/registry/moonshot.ts +7 -1
- package/src/registry/oauth/kimi.ts +17 -9
- package/src/usage/kimi.ts +10 -4
- package/src/utils/schema/normalize.ts +120 -21
- 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";
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
adaptSchemaForStrict,
|
|
39
40
|
findStrictToolSchemaViolation,
|
|
40
41
|
NO_STRICT,
|
|
42
|
+
normalizeSchemaForMoonshot,
|
|
41
43
|
sanitizeSchemaForOpenAIResponses,
|
|
42
44
|
toolWireSchema,
|
|
43
45
|
} from "../utils/schema";
|
|
@@ -154,6 +156,33 @@ const OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE =
|
|
|
154
156
|
"OpenAI responses stream timed out while waiting for the first event";
|
|
155
157
|
/** Consecutive stale-previous-response failures before chaining is disabled for the session. */
|
|
156
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
|
+
}
|
|
157
186
|
|
|
158
187
|
interface OpenAIResponsesProviderSessionState
|
|
159
188
|
extends ProviderSessionState,
|
|
@@ -452,7 +481,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
452
481
|
return payload;
|
|
453
482
|
};
|
|
454
483
|
chained = { ...chained, params: await applyPayloadReplacement(chained.params) };
|
|
455
|
-
|
|
484
|
+
const activeRawRequestDump: RawHttpRequestDump = {
|
|
456
485
|
provider: model.provider,
|
|
457
486
|
api: output.api,
|
|
458
487
|
model: model.id,
|
|
@@ -460,6 +489,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
460
489
|
url: requestUrl,
|
|
461
490
|
body: chained.params,
|
|
462
491
|
};
|
|
492
|
+
rawRequestDump = activeRawRequestDump;
|
|
463
493
|
const openResponsesStream = (requestParams: OpenAIResponsesSamplingParams) => {
|
|
464
494
|
activeReasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
465
495
|
"responses",
|
|
@@ -507,187 +537,257 @@ const streamOpenAIResponsesOnce = (
|
|
|
507
537
|
{ provider: model.provider, signal: requestSignal },
|
|
508
538
|
);
|
|
509
539
|
};
|
|
510
|
-
let openaiStream: AsyncIterable<ResponseStreamEvent>;
|
|
511
540
|
let strictRetryAvailable = true;
|
|
512
541
|
let activeStrictToolsApplied = builtParams.strictToolsApplied;
|
|
513
542
|
let forceDisableStrictTools = false;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
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
|
-
|
|
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(
|
|
566
647
|
model,
|
|
567
648
|
context,
|
|
568
649
|
options,
|
|
569
650
|
providerSessionState,
|
|
570
651
|
strictToolsScope,
|
|
571
|
-
|
|
652
|
+
forceDisableStrictTools,
|
|
572
653
|
);
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
chained = fallbackChained;
|
|
585
|
-
rawRequestDump.body = chained.params;
|
|
586
|
-
activeParams = fallbackParams;
|
|
587
|
-
activeStrictToolsApplied = fallbackBuilt.strictToolsApplied;
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
if (!chainState || !sentPreviousResponseId || requestSignal.aborted) {
|
|
591
|
-
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;
|
|
592
665
|
}
|
|
593
|
-
const zdrRejection =
|
|
594
|
-
error instanceof Error &&
|
|
595
|
-
/previous[ _]?response/i.test(error.message) &&
|
|
596
|
-
/zero[ _-]?data[ _-]?retention/i.test(error.message);
|
|
597
|
-
const isPromptBlocked =
|
|
598
|
-
error instanceof Error &&
|
|
599
|
-
((error as { code?: string }).code === "invalid_prompt" ||
|
|
600
|
-
/invalid_prompt|Request blocked/i.test(error.message));
|
|
601
|
-
if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
|
602
|
-
throw error;
|
|
603
|
-
}
|
|
604
|
-
// Server rejected the chain baseline: reset, count the failure (or
|
|
605
|
-
// disable categorically on ZDR), and retry once with the full
|
|
606
|
-
// transcript. Structurally cannot loop — the retry carries no
|
|
607
|
-
// previous_response_id.
|
|
608
|
-
if (zdrRejection) {
|
|
609
|
-
markOpenAIResponsesChainZeroDataRetention(chainState, error);
|
|
610
|
-
// ZDR orgs cannot store responses; the retry uses `store: false`.
|
|
611
|
-
} else {
|
|
612
|
-
registerOpenAIResponsesChainStaleFailure(chainState, error);
|
|
613
|
-
}
|
|
614
|
-
sentPreviousResponseId = undefined;
|
|
615
|
-
const currentBuilt = buildParams(
|
|
616
|
-
model,
|
|
617
|
-
context,
|
|
618
|
-
options,
|
|
619
|
-
providerSessionState,
|
|
620
|
-
strictToolsScope,
|
|
621
|
-
forceDisableStrictTools,
|
|
622
|
-
);
|
|
623
|
-
const currentParams = currentBuilt.params;
|
|
624
|
-
// Only ZDR forces `store: false` (the org never persists responses). A
|
|
625
|
-
// non-ZDR stale baseline is transient, so keep storing: the full-context
|
|
626
|
-
// retry must be chainable next turn, and the consecutive stale-failure
|
|
627
|
-
// breaker only trips when each retry stores and the next turn re-chains.
|
|
628
|
-
currentParams.store = !zdrRejection;
|
|
629
|
-
const retryParams = await applyPayloadReplacement(currentParams);
|
|
630
|
-
chained = { params: retryParams };
|
|
631
|
-
rawRequestDump.body = retryParams;
|
|
632
|
-
activeParams = currentParams;
|
|
633
|
-
activeStrictToolsApplied = currentBuilt.strictToolsApplied;
|
|
634
666
|
}
|
|
635
|
-
|
|
667
|
+
return openaiStream;
|
|
668
|
+
};
|
|
669
|
+
let openaiStream = await openResponsesStreamWithFallbacks();
|
|
636
670
|
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
637
671
|
stream.push({ type: "start", partial: output });
|
|
638
672
|
|
|
639
673
|
const nativeOutputItems: Array<Record<string, unknown>> = [];
|
|
640
|
-
let
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
})();
|
|
665
708
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
+
});
|
|
673
724
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// this guard the incomplete output is silently surfaced as a successful
|
|
678
|
-
// "stop".
|
|
679
|
-
if (!sawTerminalResponseEvent) {
|
|
680
|
-
throw new AIError.ProviderResponseError(
|
|
681
|
-
"OpenAI responses stream closed before a terminal response event was received",
|
|
682
|
-
{ provider: model.provider, kind: "incomplete-stream" },
|
|
683
|
-
);
|
|
684
|
-
}
|
|
725
|
+
const localAbortReason = abortTracker.getLocalAbortReason();
|
|
726
|
+
if (localAbortReason) throw localAbortReason;
|
|
727
|
+
if (abortTracker.wasCallerAbort()) throw new AIError.AbortError();
|
|
685
728
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
691
791
|
}
|
|
692
792
|
|
|
693
793
|
output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
|
|
@@ -999,7 +1099,15 @@ export function convertTools(
|
|
|
999
1099
|
}
|
|
1000
1100
|
const strict = !NO_STRICT && strictMode && tool.strict !== false;
|
|
1001
1101
|
const baseParameters = toolWireSchema(tool);
|
|
1002
|
-
|
|
1102
|
+
// MFJS must run AFTER the Responses sanitizer: the sanitizer normalizes
|
|
1103
|
+
// `{}` → `true` (issue #1179), and Moonshot's validator rejects boolean
|
|
1104
|
+
// subschemas ("property schema … must be an object"), so the Moonshot
|
|
1105
|
+
// pass re-coerces them last.
|
|
1106
|
+
const sanitized = sanitizeSchemaForOpenAIResponses(baseParameters);
|
|
1107
|
+
const responseParameters =
|
|
1108
|
+
model.compat.toolSchemaFlavor === "moonshot-mfjs"
|
|
1109
|
+
? (normalizeSchemaForMoonshot(sanitized) as Record<string, unknown>)
|
|
1110
|
+
: sanitized;
|
|
1003
1111
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
|
|
1004
1112
|
// Quarantine a tool whose emitted schema carries a provider-rejecting
|
|
1005
1113
|
// enum/const-vs-type contradiction: dropping just that tool keeps the rest
|
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { scheduler } from "node:timers/promises";
|
|
10
|
-
import { $env, getAgentDir
|
|
10
|
+
import { $env, getAgentDir } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
12
12
|
import * as AIError from "../../error";
|
|
13
13
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
@@ -57,21 +57,29 @@ function getDeviceModel(): string {
|
|
|
57
57
|
return formatDeviceModel(label, release, arch);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Device id identifies this install to Kimi. Persistence is best-effort: a
|
|
61
|
+
// missing/unwritable agent dir must never break header construction (and with
|
|
62
|
+
// it every usage probe / request that spreads getKimiCommonHeaders()) — fall
|
|
63
|
+
// back to a per-process ephemeral id instead.
|
|
60
64
|
let getDeviceId = (): string => {
|
|
61
65
|
const deviceIdPath = path.join(getAgentDir(), DEVICE_ID_FILENAME);
|
|
62
66
|
try {
|
|
63
|
-
const existing = fs.readFileSync(deviceIdPath, "utf-8");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return trimmed;
|
|
67
|
+
const existing = fs.readFileSync(deviceIdPath, "utf-8").trim();
|
|
68
|
+
if (existing) {
|
|
69
|
+
getDeviceId = () => existing;
|
|
70
|
+
return existing;
|
|
68
71
|
}
|
|
69
|
-
} catch
|
|
70
|
-
|
|
72
|
+
} catch {
|
|
73
|
+
// Unreadable device-id file: regenerate below.
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
const deviceId = crypto.randomUUID().replace(/-/g, "");
|
|
74
|
-
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(path.dirname(deviceIdPath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(deviceIdPath, `${deviceId}\n`, { mode: 0o600 });
|
|
80
|
+
} catch {
|
|
81
|
+
// Persist failure → ephemeral id for this process.
|
|
82
|
+
}
|
|
75
83
|
getDeviceId = () => deviceId;
|
|
76
84
|
return deviceId;
|
|
77
85
|
};
|
package/src/usage/kimi.ts
CHANGED
|
@@ -144,15 +144,21 @@ function buildUsageStatus(amount: UsageAmount): UsageStatus {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
// Kimi puts `resetTime` on the limit `detail`, not on `window`, so a
|
|
148
|
+
// window built from `duration`/`timeUnit` alone carries no resetsAt.
|
|
149
|
+
// Fall back to the row-level reset so `omp usage` can render
|
|
150
|
+
// "resets in …" for the 5h window too.
|
|
151
|
+
const window: UsageWindow | undefined = row.window
|
|
152
|
+
? row.window.resetsAt !== undefined || row.resetsAt === undefined
|
|
153
|
+
? row.window
|
|
154
|
+
: { ...row.window, resetsAt: row.resetsAt }
|
|
155
|
+
: row.resetsAt
|
|
150
156
|
? {
|
|
151
157
|
id: "default",
|
|
152
158
|
label: "Usage window",
|
|
153
159
|
resetsAt: row.resetsAt,
|
|
154
160
|
}
|
|
155
|
-
: undefined
|
|
161
|
+
: undefined;
|
|
156
162
|
|
|
157
163
|
const amount = buildUsageAmount(row);
|
|
158
164
|
return {
|