@opengeni/react 1.4.4 → 1.4.5-canary.37458084490001
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/README.md +17 -6
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +22 -15
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-5EEAS3IU.js → browser-viewer-CV23VIHB.js} +8 -6
- package/dist/chunk-3ZC6QPOM.js +174 -0
- package/dist/chunk-3ZC6QPOM.js.map +1 -0
- package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
- package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
- package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
- package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
- package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
- package/dist/chunk-7RRBX5EM.js.map +1 -0
- package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
- package/dist/chunk-7ZB7I3KP.js.map +1 -0
- package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
- package/dist/chunk-ATIRUHBZ.js.map +1 -0
- package/dist/chunk-BOFEQC6K.js +162 -0
- package/dist/chunk-BOFEQC6K.js.map +1 -0
- package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
- package/dist/chunk-CESBPQP5.js.map +1 -0
- package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
- package/dist/chunk-GHJU7XG7.js.map +1 -0
- package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
- package/dist/chunk-HNVWMDNN.js.map +1 -0
- package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
- package/dist/chunk-I5EGRXXG.js.map +1 -0
- package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
- package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
- package/dist/chunk-IX3K4THH.js.map +1 -0
- package/dist/chunk-IZV5FTJC.js +55 -0
- package/dist/chunk-IZV5FTJC.js.map +1 -0
- package/dist/chunk-JDJJMBB3.js +1883 -0
- package/dist/chunk-JDJJMBB3.js.map +1 -0
- package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
- package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
- package/dist/chunk-LHF6BVM4.js +83 -0
- package/dist/chunk-LHF6BVM4.js.map +1 -0
- package/dist/chunk-N7S72EXK.js +25 -0
- package/dist/chunk-N7S72EXK.js.map +1 -0
- package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
- package/dist/chunk-PEHTX3V7.js.map +1 -0
- package/dist/chunk-R5R5RQQD.js +51 -0
- package/dist/chunk-R5R5RQQD.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
- package/dist/chunk-SHLZMAJN.js.map +1 -0
- package/dist/{chunk-EAOSEIXB.js → chunk-TZQSHCZN.js} +692 -1996
- package/dist/chunk-TZQSHCZN.js.map +1 -0
- package/dist/chunk-ZIB4H5LN.js +101 -0
- package/dist/chunk-ZIB4H5LN.js.map +1 -0
- package/dist/client.d.ts +9 -3
- package/dist/clipboard.js +1 -1
- package/dist/components/browser-viewer.d.ts +6 -0
- package/dist/components/composer.d.ts +3 -69
- package/dist/components/human-input-form.d.ts +3 -32
- package/dist/components/markdown.d.ts +8 -0
- package/dist/components/model-policy-picker.d.ts +4 -37
- package/dist/components/opper-mark.d.ts +7 -0
- package/dist/components/provider-recovery-notice.d.ts +15 -0
- package/dist/components/remark-soft-line-breaks.d.ts +38 -0
- package/dist/components/sandbox-workspace.d.ts +13 -0
- package/dist/components/session-chrome.d.ts +5 -3
- package/dist/components/session-conversation.d.ts +15 -3
- package/dist/composer-messages.d.ts +74 -0
- package/dist/composer-messages.js +9 -0
- package/dist/composer.js +15 -10
- package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
- package/dist/connect-setup.d.ts +2 -0
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +21 -6
- package/dist/connect.js.map +1 -1
- package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
- package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
- package/dist/hooks/shared-machines-list.d.ts +4 -0
- package/dist/hooks/use-client-config-flags.d.ts +6 -0
- package/dist/hooks/use-goal.d.ts +2 -1
- package/dist/human-input-model.d.ts +40 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +116 -73
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +5 -5
- package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/provider-recovery.d.ts +49 -0
- package/dist/machines.js +2 -2
- package/dist/model-mark-paths.d.ts +34 -0
- package/dist/model-mark-paths.js +16 -0
- package/dist/model-picker-model.d.ts +111 -0
- package/dist/model-picker-model.js +25 -0
- package/dist/model-picker-model.js.map +1 -0
- package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
- package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
- package/dist/model-policy.d.ts +10 -0
- package/dist/model-policy.js +5 -1
- package/dist/organization-model.d.ts +32 -0
- package/dist/organization-model.js +65 -0
- package/dist/organization-model.js.map +1 -0
- package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
- package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
- package/dist/queue-presentation-model.d.ts +72 -0
- package/dist/queue-presentation-model.js +38 -0
- package/dist/queue-presentation-model.js.map +1 -0
- package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
- package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
- package/dist/realtime.js +5 -3
- package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
- package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
- package/dist/sandbox-label-model.d.ts +23 -0
- package/dist/sandbox-label-model.js +28 -0
- package/dist/sandbox-label-model.js.map +1 -0
- package/dist/session-attention-model.d.ts +8 -0
- package/dist/session-attention-model.js +46 -0
- package/dist/session-attention-model.js.map +1 -0
- package/dist/session-feedback-model.d.ts +20 -0
- package/dist/session-feedback-model.js +56 -0
- package/dist/session-feedback-model.js.map +1 -0
- package/dist/session-list-model.d.ts +96 -0
- package/dist/session-list-model.js +219 -0
- package/dist/session-list-model.js.map +1 -0
- package/dist/session-status-model.d.ts +19 -0
- package/dist/session-ui.js +26 -17
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +8 -6
- package/dist/testing/session-scenarios.d.ts +10 -0
- package/dist/testing/session-scenarios.js +303 -0
- package/dist/testing/session-scenarios.js.map +1 -0
- package/dist/timeline/genie-copy.d.ts +3 -0
- package/dist/timeline/genie-loading.d.ts +11 -0
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/notice-presentation.d.ts +7 -0
- package/dist/timeline/patch-apply-command.d.ts +18 -0
- package/dist/timeline/platform-activity-presentation.d.ts +7 -0
- package/dist/timeline/presented-image.d.ts +6 -1
- package/dist/timeline/question-nav-model.d.ts +23 -0
- package/dist/timeline/registry.d.ts +10 -1
- package/dist/timeline/retained-loaders.d.ts +1 -1
- package/dist/timeline/tool-presentation.d.ts +172 -0
- package/dist/timeline/turn-summary-model.d.ts +90 -0
- package/dist/timeline/turn-summary.d.ts +4 -74
- package/dist/timeline/work-presentation.d.ts +42 -0
- package/dist/timeline-model.d.ts +24 -0
- package/dist/timeline-model.js +152 -0
- package/dist/timeline-model.js.map +1 -0
- package/package.json +47 -3
- package/src/artifacts.ts +1 -0
- package/src/client.ts +9 -3
- package/src/components/browser-viewer.tsx +23 -2
- package/src/components/claude-mark.tsx +2 -1
- package/src/components/composer.tsx +4 -144
- package/src/components/grok-mark.tsx +2 -1
- package/src/components/human-input-form.tsx +13 -124
- package/src/components/markdown.tsx +19 -1
- package/src/components/message-timeline.tsx +45 -99
- package/src/components/model-mark.tsx +2 -1
- package/src/components/model-policy-picker-menu.tsx +30 -85
- package/src/components/model-policy-picker.tsx +15 -148
- package/src/components/opper-mark.tsx +18 -0
- package/src/components/provider-recovery-notice.tsx +41 -0
- package/src/components/queue-surface-implementation.tsx +20 -349
- package/src/components/remark-soft-line-breaks.ts +166 -0
- package/src/components/sandbox-workspace.tsx +17 -1
- package/src/components/session-chrome.tsx +114 -99
- package/src/components/session-conversation.tsx +55 -6
- package/src/components/session-status.tsx +17 -16
- package/src/composer-messages.ts +150 -0
- package/src/connect-setup.tsx +45 -11
- package/src/connect.ts +1 -1
- package/src/hooks/shared-machines-list.ts +34 -1
- package/src/hooks/use-client-config-flags.ts +8 -0
- package/src/hooks/use-goal.ts +66 -10
- package/src/human-input-model.ts +147 -0
- package/src/index.ts +16 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/format.ts +7 -0
- package/src/lib/provider-recovery.ts +202 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +43 -2
- package/src/organization-model.ts +118 -0
- package/src/queue-presentation-model.ts +464 -0
- package/src/sandbox-label-model.ts +41 -0
- package/src/session-attention-model.ts +59 -0
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +327 -0
- package/src/session-status-model.ts +62 -0
- package/src/testing/session-scenarios.ts +326 -0
- package/src/timeline/activity-rail.tsx +5 -17
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +13 -18
- package/src/timeline/index.ts +1 -0
- package/src/timeline/notice-presentation.ts +22 -0
- package/src/timeline/patch-apply-command.tsx +135 -0
- package/src/timeline/platform-activity-presentation.ts +122 -0
- package/src/timeline/platform-activity-row.tsx +6 -96
- package/src/timeline/presented-image.ts +16 -2
- package/src/timeline/question-nav-model.ts +28 -0
- package/src/timeline/registry.ts +11 -1
- package/src/timeline/retained-loaders.ts +9 -4
- package/src/timeline/rolling-activity.tsx +7 -19
- package/src/timeline/tool-presentation.ts +1316 -0
- package/src/timeline/tool-renderers.tsx +226 -1025
- package/src/timeline/turn-summary-model.ts +338 -0
- package/src/timeline/turn-summary.tsx +21 -326
- package/src/timeline/work-presentation.ts +139 -0
- package/src/timeline-model.ts +108 -0
- package/styles/compiled.css +3 -0
- package/styles/connect.css +4 -0
- package/dist/chunk-2OQA3CPB.js.map +0 -1
- package/dist/chunk-CVYCLFMH.js.map +0 -1
- package/dist/chunk-EAOSEIXB.js.map +0 -1
- package/dist/chunk-G6YRCIYJ.js.map +0 -1
- package/dist/chunk-HXYOSSQO.js +0 -117
- package/dist/chunk-HXYOSSQO.js.map +0 -1
- package/dist/chunk-IUHHPMAX.js.map +0 -1
- package/dist/chunk-IYQCWGU5.js.map +0 -1
- package/dist/chunk-JH7V627O.js.map +0 -1
- package/dist/chunk-L2J3PU7A.js.map +0 -1
- package/dist/chunk-LVS3MH5T.js.map +0 -1
- package/dist/chunk-PHBFJCEU.js.map +0 -1
- package/dist/chunk-XS67YG5R.js.map +0 -1
- package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
- package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
- /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
- /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
- /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
- /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
- /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
- /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
- /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
- /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
- /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -401,6 +401,7 @@ export {
|
|
|
401
401
|
export type {
|
|
402
402
|
CreateToolRegistryOptions,
|
|
403
403
|
RetainedArtifactLoader,
|
|
404
|
+
RetainedArtifactLoadOptions,
|
|
404
405
|
RetainedArtifactLoaderClient,
|
|
405
406
|
RetainedScreenshotLoader,
|
|
406
407
|
RetainedScreenshotLoaderClient,
|
|
@@ -716,6 +717,20 @@ export {
|
|
|
716
717
|
truncate,
|
|
717
718
|
tryParseJson,
|
|
718
719
|
} from "./lib/format";
|
|
720
|
+
export {
|
|
721
|
+
currentProviderRecovery,
|
|
722
|
+
parseProviderRecovery,
|
|
723
|
+
providerRecoveryExhaustedText,
|
|
724
|
+
providerRecoveryRetryingText,
|
|
725
|
+
providerRecoverySubject,
|
|
726
|
+
type ProviderRecoveryCondition,
|
|
727
|
+
type ProviderRecoveryFacts,
|
|
728
|
+
} from "./lib/provider-recovery";
|
|
729
|
+
export {
|
|
730
|
+
ProviderRecoveryNotice,
|
|
731
|
+
PROVIDER_RECOVERY_NOTICE_DETAIL,
|
|
732
|
+
type ProviderRecoveryNoticeProps,
|
|
733
|
+
} from "./components/provider-recovery-notice";
|
|
719
734
|
export { SessionCommandsPanel } from "./components/session-commands-panel";
|
|
720
735
|
export { SessionCommands } from "./components/session-commands";
|
|
721
736
|
|
|
@@ -728,6 +743,7 @@ export { setStartupDetails, useStartupDetails } from "./timeline/startup-prefere
|
|
|
728
743
|
export { ClaudeMark } from "./components/claude-mark";
|
|
729
744
|
export { AnthropicMark } from "./components/anthropic-mark";
|
|
730
745
|
export { OpenRouterMark } from "./components/openrouter-mark";
|
|
746
|
+
export { OpperMark } from "./components/opper-mark";
|
|
731
747
|
export { GrokMark } from "./components/grok-mark";
|
|
732
748
|
|
|
733
749
|
export { ToolReviewHistoryProvider } from "./components/tool-review-history";
|
package/src/lib/clipboard.ts
CHANGED
|
@@ -35,6 +35,35 @@ export async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Copy text that is still being prepared, such as a command around a freshly
|
|
40
|
+
* minted URL. Safari only allows a clipboard write during the click itself, so
|
|
41
|
+
* hand the pending text to a `ClipboardItem` where supported; otherwise wait
|
|
42
|
+
* for it and copy. Resolves false when the text failed or nothing was copied.
|
|
43
|
+
*/
|
|
44
|
+
export async function copyPendingTextToClipboard(text: Promise<string>): Promise<boolean> {
|
|
45
|
+
if (
|
|
46
|
+
typeof ClipboardItem !== "undefined" &&
|
|
47
|
+
typeof navigator !== "undefined" &&
|
|
48
|
+
typeof navigator.clipboard?.write === "function"
|
|
49
|
+
) {
|
|
50
|
+
const blob = text.then((value) => new Blob([value], { type: "text/plain" }));
|
|
51
|
+
// The caller observes a failed `text`; never leave this copy unhandled.
|
|
52
|
+
blob.catch(() => {});
|
|
53
|
+
try {
|
|
54
|
+
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall through: some browsers reject promised clipboard items.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await copyTextToClipboard(await text);
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
/** Serialize an HTML table to tab-separated values (spreadsheet-friendly). */
|
|
39
68
|
export function tableElementToTsv(table: HTMLTableElement | null | undefined): string {
|
|
40
69
|
if (!table) {
|
package/src/lib/format.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatErrorMessage, OpenGeniApiError } from "@opengeni/sdk";
|
|
2
|
+
import { parseProviderRecovery, providerRecoveryExhaustedText } from "./provider-recovery";
|
|
2
3
|
|
|
3
4
|
const clockTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
4
5
|
month: "short",
|
|
@@ -312,6 +313,12 @@ export function presentFailure(payload: Record<string, unknown>): {
|
|
|
312
313
|
safetyRefusal: true,
|
|
313
314
|
};
|
|
314
315
|
}
|
|
316
|
+
// Spent automatic retries: name the model and the remedy instead of the
|
|
317
|
+
// recorded wrapper plus raw provider text (still in the stored event).
|
|
318
|
+
const recovery = parseProviderRecovery(payload);
|
|
319
|
+
if (recovery && payload.recoveryExhausted === true) {
|
|
320
|
+
return { reason: providerRecoveryExhaustedText(recovery), safetyRefusal: false };
|
|
321
|
+
}
|
|
315
322
|
return {
|
|
316
323
|
reason:
|
|
317
324
|
detail && detail !== message
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { SessionEvent, SessionStatus } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Finite same-turn provider recovery, projected for people. Presentation only:
|
|
5
|
+
* the worker owns retry authority, pacing and the recovery budget. The worker
|
|
6
|
+
* (`apps/worker/src/activities/agent-turn/provider-recovery-copy.ts`) writes
|
|
7
|
+
* the same exhausted sentence into `turn.failed.error`; keep the two in step.
|
|
8
|
+
*/
|
|
9
|
+
export type ProviderRecoveryCondition =
|
|
10
|
+
| "overloaded"
|
|
11
|
+
| "unavailable"
|
|
12
|
+
| "unresponsive"
|
|
13
|
+
| "rate_limited";
|
|
14
|
+
|
|
15
|
+
export type ProviderRecoveryFacts = {
|
|
16
|
+
/** Recorded worker failure code (`provider_unavailable`, `mcp_transport_timeout`, ...). */
|
|
17
|
+
code: string;
|
|
18
|
+
condition: ProviderRecoveryCondition | null;
|
|
19
|
+
/** Display label of the affected model, when the route was recorded. */
|
|
20
|
+
modelLabel: string | null;
|
|
21
|
+
/** Display label of the serving provider, when the route was recorded. */
|
|
22
|
+
providerLabel: string | null;
|
|
23
|
+
/** Automatic retries scheduled (live) or spent (exhausted). */
|
|
24
|
+
attempt: number | null;
|
|
25
|
+
/** Finite automatic retry budget. */
|
|
26
|
+
maxAttempts: number | null;
|
|
27
|
+
/** True when the model route itself is affected, so another model may help. */
|
|
28
|
+
modelRoute: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MODEL_CODES: ReadonlySet<string> = new Set([
|
|
32
|
+
"provider_unavailable",
|
|
33
|
+
"provider_rate_limited",
|
|
34
|
+
"provider_unknown_finish_reason",
|
|
35
|
+
"post_compaction_continuation_empty",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const RECOVERY_CODES: ReadonlySet<string> = new Set([
|
|
39
|
+
...MODEL_CODES,
|
|
40
|
+
"upstream_connectivity_unavailable",
|
|
41
|
+
"mcp_transport_timeout",
|
|
42
|
+
"mcp_transport_unavailable",
|
|
43
|
+
"sandbox_command_start_unavailable",
|
|
44
|
+
"turn_execution_policy_definition_mismatch",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Matches the worker's default budget for legacy events that omit it. */
|
|
48
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
49
|
+
|
|
50
|
+
function text(value: unknown, max = 120): string | null {
|
|
51
|
+
if (typeof value !== "string") return null;
|
|
52
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
53
|
+
return normalized ? normalized.slice(0, max) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function count(value: unknown): number | null {
|
|
57
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function condition(
|
|
61
|
+
payload: Record<string, unknown>,
|
|
62
|
+
code: string,
|
|
63
|
+
): ProviderRecoveryCondition | null {
|
|
64
|
+
const recorded = payload.providerCondition;
|
|
65
|
+
if (
|
|
66
|
+
recorded === "overloaded" ||
|
|
67
|
+
recorded === "unavailable" ||
|
|
68
|
+
recorded === "unresponsive" ||
|
|
69
|
+
recorded === "rate_limited"
|
|
70
|
+
)
|
|
71
|
+
return recorded;
|
|
72
|
+
if (code === "provider_rate_limited") return "rate_limited";
|
|
73
|
+
if (code !== "provider_unavailable") return null;
|
|
74
|
+
// Older events predate the typed condition; their provider text still says so.
|
|
75
|
+
return [payload.lastRetryableError, payload.detail, payload.error].some(
|
|
76
|
+
(value) => typeof value === "string" && /\boverload(?:ed)?(?:_error)?\b/i.test(value),
|
|
77
|
+
)
|
|
78
|
+
? "overloaded"
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read a retryable `turn.recovery.requested` payload or an exhausted
|
|
84
|
+
* `turn.failed` payload. Returns null for every other recovery reason (human
|
|
85
|
+
* Retry, worker shutdown, credential rotation, capacity waits, ...).
|
|
86
|
+
*/
|
|
87
|
+
export function parseProviderRecovery(payload: unknown): ProviderRecoveryFacts | null {
|
|
88
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
89
|
+
const record = payload as Record<string, unknown>;
|
|
90
|
+
const code = text(record.code) ?? text(record.reason);
|
|
91
|
+
if (!code || !RECOVERY_CODES.has(code)) return null;
|
|
92
|
+
// A live recovery request is retryable; a spent budget is explicitly marked.
|
|
93
|
+
if (record.recoveryExhausted !== true && record.retryable === false) return null;
|
|
94
|
+
const attempt = count(record.providerRecoveryCount);
|
|
95
|
+
return {
|
|
96
|
+
code,
|
|
97
|
+
condition: condition(record, code),
|
|
98
|
+
modelLabel: text(record.modelLabel),
|
|
99
|
+
providerLabel: text(record.providerLabel),
|
|
100
|
+
attempt,
|
|
101
|
+
maxAttempts: count(record.maxProviderRecoveryCount) ?? DEFAULT_MAX_ATTEMPTS,
|
|
102
|
+
modelRoute: MODEL_CODES.has(code),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One present-tense clause naming what is unavailable, without a trailing period. */
|
|
107
|
+
export function providerRecoverySubject(facts: ProviderRecoveryFacts): string {
|
|
108
|
+
const model = facts.modelLabel ?? "The model";
|
|
109
|
+
const at = facts.providerLabel ? ` at the provider (${facts.providerLabel})` : " at the provider";
|
|
110
|
+
switch (facts.code) {
|
|
111
|
+
case "provider_rate_limited":
|
|
112
|
+
return `${model} is rate limited${at}`;
|
|
113
|
+
case "provider_unavailable":
|
|
114
|
+
return facts.condition === "overloaded"
|
|
115
|
+
? `${model} is overloaded${at}`
|
|
116
|
+
: facts.condition === "unresponsive"
|
|
117
|
+
? `${model} stopped responding${at}`
|
|
118
|
+
: `${model} is temporarily unavailable${at}`;
|
|
119
|
+
case "provider_unknown_finish_reason":
|
|
120
|
+
return `${model} ended its response unexpectedly`;
|
|
121
|
+
case "post_compaction_continuation_empty":
|
|
122
|
+
return `${model} stopped right after compacting the conversation`;
|
|
123
|
+
case "upstream_connectivity_unavailable":
|
|
124
|
+
return "Opengeni can't reach an upstream service";
|
|
125
|
+
case "mcp_transport_timeout":
|
|
126
|
+
return "A required MCP server isn't responding";
|
|
127
|
+
case "mcp_transport_unavailable":
|
|
128
|
+
return "A required MCP server is unreachable";
|
|
129
|
+
case "sandbox_command_start_unavailable":
|
|
130
|
+
return "The sandbox isn't ready yet";
|
|
131
|
+
case "turn_execution_policy_definition_mismatch":
|
|
132
|
+
return "Opengeni is applying a configuration update";
|
|
133
|
+
default:
|
|
134
|
+
return "A service this turn depends on is temporarily unavailable";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Live status while the same turn waits for its next automatic retry. */
|
|
139
|
+
export function providerRecoveryRetryingText(facts: ProviderRecoveryFacts): string {
|
|
140
|
+
const progress =
|
|
141
|
+
facts.attempt !== null && facts.maxAttempts !== null
|
|
142
|
+
? ` (attempt ${Math.min(facts.attempt, facts.maxAttempts)} of ${facts.maxAttempts})`
|
|
143
|
+
: " automatically";
|
|
144
|
+
return `${providerRecoverySubject(facts)} — retrying${progress}…`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Terminal sentence once the automatic budget is spent. */
|
|
148
|
+
export function providerRecoveryExhaustedText(facts: ProviderRecoveryFacts): string {
|
|
149
|
+
const spent = facts.attempt ?? facts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
150
|
+
const retried = `Opengeni retried ${spent} ${spent === 1 ? "time" : "times"} without success.`;
|
|
151
|
+
const remedy = facts.modelRoute
|
|
152
|
+
? "Try again in a few minutes, or switch to another model."
|
|
153
|
+
: "Try again in a few minutes.";
|
|
154
|
+
return `${providerRecoverySubject(facts)}. ${retried} ${remedy}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const BOUNDARY_TYPES: ReadonlySet<string> = new Set([
|
|
158
|
+
"turn.recovery.requested",
|
|
159
|
+
"turn.started",
|
|
160
|
+
"turn.completed",
|
|
161
|
+
"turn.failed",
|
|
162
|
+
"turn.cancelled",
|
|
163
|
+
"turn.superseded",
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The active turn's current automatic provider retry, or null. Only a
|
|
168
|
+
* `recovering`, unpaused session qualifies, and only the newest boundary of its
|
|
169
|
+
* exact active turn counts, so merged or replayed pages never borrow another
|
|
170
|
+
* turn's diagnosis.
|
|
171
|
+
*/
|
|
172
|
+
export function currentProviderRecovery(
|
|
173
|
+
session: {
|
|
174
|
+
id: string;
|
|
175
|
+
status: SessionStatus | null | undefined;
|
|
176
|
+
activeTurnId: string | null | undefined;
|
|
177
|
+
effectiveControl?: { state: string } | null | undefined;
|
|
178
|
+
},
|
|
179
|
+
events: readonly SessionEvent[],
|
|
180
|
+
): ProviderRecoveryFacts | null {
|
|
181
|
+
if (
|
|
182
|
+
session.status !== "recovering" ||
|
|
183
|
+
(session.effectiveControl && session.effectiveControl.state !== "active") ||
|
|
184
|
+
!session.activeTurnId
|
|
185
|
+
)
|
|
186
|
+
return null;
|
|
187
|
+
let latest: SessionEvent | undefined;
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
if (
|
|
190
|
+
event.sessionId !== session.id ||
|
|
191
|
+
event.turnId !== session.activeTurnId ||
|
|
192
|
+
event.duplicateOfEventId ||
|
|
193
|
+
(event.turnAssociation && event.turnAssociation !== "current") ||
|
|
194
|
+
!BOUNDARY_TYPES.has(event.type)
|
|
195
|
+
)
|
|
196
|
+
continue;
|
|
197
|
+
if (!latest || event.sequence > latest.sequence) latest = event;
|
|
198
|
+
}
|
|
199
|
+
if (latest?.type !== "turn.recovery.requested") return null;
|
|
200
|
+
const payload = latest.payload as Record<string, unknown> | null | undefined;
|
|
201
|
+
return payload?.recoveryExhausted === true ? null : parseProviderRecovery(payload);
|
|
202
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Model maker marks as plain path data (24×24 viewBox, filled with the current
|
|
2
|
+
// color), shared by the web SVG components and native renderers so both draw
|
|
3
|
+
// identical geometry.
|
|
4
|
+
import { modelVendor, type ModelDisplayInput, type ModelVendor } from "@opengeni/sdk/model-display";
|
|
5
|
+
|
|
6
|
+
/** ChatGPT / OpenAI mark (Simple Icons, CC0). */
|
|
7
|
+
export const CHATGPT_MARK_PATH =
|
|
8
|
+
"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3653-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8414 3.3698-2.02 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.783-2.7622a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z";
|
|
9
|
+
|
|
10
|
+
/** Claude mark (Simple Icons, CC0; https://github.com/simple-icons/simple-icons). */
|
|
11
|
+
export const CLAUDE_MARK_PATH =
|
|
12
|
+
"m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MIT License
|
|
16
|
+
*
|
|
17
|
+
* Copyright (c) 2023 LobeHub
|
|
18
|
+
*
|
|
19
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
* in the Software without restriction, including without limitation the rights
|
|
22
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
* furnished to do so, subject to the following conditions:
|
|
25
|
+
*
|
|
26
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
* copies or substantial portions of the Software.
|
|
28
|
+
*
|
|
29
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
* SOFTWARE.
|
|
36
|
+
* Source: https://raw.githubusercontent.com/lobehub/lobe-icons/master/src/Grok/components/Mono.tsx
|
|
37
|
+
*/
|
|
38
|
+
export const GROK_MARK_PATH =
|
|
39
|
+
"M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815";
|
|
40
|
+
|
|
41
|
+
export type MarkedModelVendor = Extract<ModelVendor, "openai" | "anthropic" | "xai">;
|
|
42
|
+
|
|
43
|
+
export const MODEL_MARK_PATHS: Record<MarkedModelVendor, string> = {
|
|
44
|
+
openai: CHATGPT_MARK_PATH,
|
|
45
|
+
anthropic: CLAUDE_MARK_PATH,
|
|
46
|
+
xai: GROK_MARK_PATH,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The maker whose logo marks this model, or null for the neutral fallback. */
|
|
50
|
+
export function modelMarkVendor(model: ModelDisplayInput): MarkedModelVendor | null {
|
|
51
|
+
const vendor = modelVendor(model);
|
|
52
|
+
return vendor === "openai" || vendor === "anthropic" || vendor === "xai" ? vendor : null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model picker's presentation logic, free of any renderer: which rows show,
|
|
3
|
+
* how they group and filter, and which thinking/latency controls apply. The web
|
|
4
|
+
* menu and native sheets both render from this.
|
|
5
|
+
*/
|
|
6
|
+
import type { ClientModel, LatencyMode, ReasoningEffort } from "@opengeni/sdk";
|
|
7
|
+
import {
|
|
8
|
+
coerceReasoningEffortForModel,
|
|
9
|
+
effortOptionsForModel,
|
|
10
|
+
findPickerRow,
|
|
11
|
+
groupPickerRowsByBillingClass,
|
|
12
|
+
payerSummaryForModel,
|
|
13
|
+
projectClientModelRows,
|
|
14
|
+
runnableLatencyModesForModel,
|
|
15
|
+
scopedBillingClassLabel,
|
|
16
|
+
type PickerBillingClass,
|
|
17
|
+
type PickerModelRow,
|
|
18
|
+
} from "./model-policy";
|
|
19
|
+
|
|
20
|
+
type ClientPickerModelRow = PickerModelRow<ClientModel>;
|
|
21
|
+
|
|
22
|
+
export type ModelPolicyPickerMessages = {
|
|
23
|
+
label: string;
|
|
24
|
+
loading: string;
|
|
25
|
+
noModels: string;
|
|
26
|
+
connectTitle: string;
|
|
27
|
+
connectBody: string;
|
|
28
|
+
connectAction: string;
|
|
29
|
+
thinking: string;
|
|
30
|
+
fast: string;
|
|
31
|
+
fastRateHint: string;
|
|
32
|
+
codexOnly: string;
|
|
33
|
+
searchLabel?: string;
|
|
34
|
+
searchPlaceholder?: string;
|
|
35
|
+
currentModel?: string;
|
|
36
|
+
noMatches?: string;
|
|
37
|
+
unsupportedAttachments?: string;
|
|
38
|
+
thinkingEffort?: string;
|
|
39
|
+
selected?: string;
|
|
40
|
+
free?: string;
|
|
41
|
+
|
|
42
|
+
billingHints: Record<PickerBillingClass, string>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
|
|
46
|
+
label: "Model and effort",
|
|
47
|
+
loading: "Loading model catalog…",
|
|
48
|
+
noModels: "No models available.",
|
|
49
|
+
connectTitle: "Connect a model",
|
|
50
|
+
connectBody: "Use a subscription or a provider key you already have.",
|
|
51
|
+
connectAction: "Open Models",
|
|
52
|
+
thinking: "Thinking",
|
|
53
|
+
fast: "Fast",
|
|
54
|
+
fastRateHint: "2× rate",
|
|
55
|
+
codexOnly: "Codex-only session",
|
|
56
|
+
searchLabel: "Search models or providers",
|
|
57
|
+
searchPlaceholder: "Search models or providers…",
|
|
58
|
+
currentModel: "Current model",
|
|
59
|
+
noMatches: "No matching models. Try a model or provider name.",
|
|
60
|
+
unsupportedAttachments: "This model cannot view the attached images.",
|
|
61
|
+
thinkingEffort: "Thinking effort",
|
|
62
|
+
selected: "Selected",
|
|
63
|
+
free: "Free",
|
|
64
|
+
|
|
65
|
+
billingHints: {
|
|
66
|
+
opengeni_credits: "Provided by Opengeni",
|
|
67
|
+
external: "Provider terms and limits apply",
|
|
68
|
+
codex_subscription: "ChatGPT / Codex plan",
|
|
69
|
+
supergrok_subscription: "SuperGrok / xAI plan",
|
|
70
|
+
claude_subscription: "Claude plan",
|
|
71
|
+
byok: "Billed to the connected provider account",
|
|
72
|
+
organization_byok: "Billed to the connected provider account",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Host branding for one payment group; the icon type belongs to the renderer. */
|
|
77
|
+
export type ModelPickerGroupBranding<TIcon = unknown> = {
|
|
78
|
+
label?: string | undefined;
|
|
79
|
+
/** Omit to preserve the default; null hides the supporting text. */
|
|
80
|
+
description?: string | null | undefined;
|
|
81
|
+
/** Decorative, non-interactive content. Omit for the default; null hides it. */
|
|
82
|
+
icon?: TIcon | undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type ModelPickerGroupPresentation<TIcon = unknown> = Partial<
|
|
86
|
+
Record<PickerBillingClass, ModelPickerGroupBranding<TIcon>>
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
/** The renderer-neutral inputs every picker surface shares. */
|
|
90
|
+
export type ModelPickerInput<TIcon = unknown> = {
|
|
91
|
+
groupPresentation?: ModelPickerGroupPresentation<TIcon> | undefined;
|
|
92
|
+
models?: ClientModel[] | undefined;
|
|
93
|
+
rows?: PickerModelRow[] | undefined;
|
|
94
|
+
hasImageAttachments?: boolean | undefined;
|
|
95
|
+
model: string;
|
|
96
|
+
effort: ReasoningEffort;
|
|
97
|
+
latencyMode: LatencyMode;
|
|
98
|
+
codexOnly?: boolean | undefined;
|
|
99
|
+
collapseScopes?: boolean | undefined;
|
|
100
|
+
allowLatencyMode?: boolean | undefined;
|
|
101
|
+
messages?: Partial<ModelPolicyPickerMessages> | undefined;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Models settings keeps naming who is billed for a key. */
|
|
105
|
+
export const SCOPED_BILLING_HINTS: Partial<Record<PickerBillingClass, string>> = {
|
|
106
|
+
byok: "Billed to the workspace provider account",
|
|
107
|
+
organization_byok: "Billed to the organization provider account",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export function isCodexPickerModel(model: ClientModel): boolean {
|
|
111
|
+
return model.id.startsWith("codex/") || model.source === "codex";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The payment group a selection the catalog no longer lists most likely belonged to. */
|
|
115
|
+
export function billingClassForMissingSelection(modelId: string): PickerBillingClass {
|
|
116
|
+
if (modelId.startsWith("workspace-claude-subscription/")) return "claude_subscription";
|
|
117
|
+
if (modelId.startsWith("workspace-anthropic/")) return "byok";
|
|
118
|
+
if (modelId.startsWith("organization-claude-subscription/")) return "claude_subscription";
|
|
119
|
+
if (modelId.startsWith("organization-anthropic/")) return "organization_byok";
|
|
120
|
+
if (modelId.startsWith("workspace-gateway/")) return "byok";
|
|
121
|
+
if (modelId.startsWith("workspace-openrouter/")) return "byok";
|
|
122
|
+
if (modelId.startsWith("workspace-opper/")) return "byok";
|
|
123
|
+
// A deployment OpenRouter or Opper ID does not encode its workspace-facing cost.
|
|
124
|
+
// Missing rows therefore use the credits-safe rail instead of falsely
|
|
125
|
+
// claiming that an unknown former selection was externally funded.
|
|
126
|
+
if (modelId.startsWith("openrouter/")) return "opengeni_credits";
|
|
127
|
+
if (modelId.startsWith("opper/")) return "opengeni_credits";
|
|
128
|
+
if (modelId.startsWith("codex/")) return "codex_subscription";
|
|
129
|
+
if (modelId.startsWith("supergrok/")) return "supergrok_subscription";
|
|
130
|
+
return "opengeni_credits";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyCodexOnly(
|
|
134
|
+
rows: ClientPickerModelRow[],
|
|
135
|
+
codexOnly: boolean,
|
|
136
|
+
unavailableReason: string,
|
|
137
|
+
): ClientPickerModelRow[] {
|
|
138
|
+
if (!codexOnly) return rows;
|
|
139
|
+
return rows.map((row) =>
|
|
140
|
+
isCodexPickerModel(row.catalog)
|
|
141
|
+
? row
|
|
142
|
+
: {
|
|
143
|
+
...row,
|
|
144
|
+
selectable: false,
|
|
145
|
+
unavailableReason: row.unavailableReason ?? unavailableReason,
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Host branding for a payment group. While scopes are collapsed, API keys are
|
|
152
|
+
* one group, so either API-key presentation serves both.
|
|
153
|
+
*/
|
|
154
|
+
export function groupPresentationFor<TIcon>(
|
|
155
|
+
props: {
|
|
156
|
+
groupPresentation?: ModelPickerGroupPresentation<TIcon> | undefined;
|
|
157
|
+
collapseScopes?: boolean | undefined;
|
|
158
|
+
},
|
|
159
|
+
billingClass: PickerBillingClass,
|
|
160
|
+
): ModelPickerGroupBranding<TIcon> | undefined {
|
|
161
|
+
const own = props.groupPresentation?.[billingClass];
|
|
162
|
+
if (own !== undefined || props.collapseScopes === false) return own;
|
|
163
|
+
if (billingClass === "byok") return props.groupPresentation?.organization_byok;
|
|
164
|
+
if (billingClass === "organization_byok") return props.groupPresentation?.byok;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function effectiveRows<TIcon>(props: ModelPickerInput<TIcon>): ClientPickerModelRow[] {
|
|
169
|
+
const rows = props.rows !== undefined ? props.rows : projectClientModelRows(props.models ?? []);
|
|
170
|
+
const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
|
|
171
|
+
return applyCodexOnly(rows, props.codexOnly === true, messages.codexOnly).map((row) => {
|
|
172
|
+
const scoped =
|
|
173
|
+
props.collapseScopes === false
|
|
174
|
+
? {
|
|
175
|
+
...row,
|
|
176
|
+
label: row.catalog.label,
|
|
177
|
+
billingClassLabel: scopedBillingClassLabel(row.billingClass),
|
|
178
|
+
}
|
|
179
|
+
: row;
|
|
180
|
+
if (props.collapseScopes === false) {
|
|
181
|
+
if (row.catalog.shortLabel) scoped.shortLabel = row.catalog.shortLabel;
|
|
182
|
+
else delete scoped.shortLabel;
|
|
183
|
+
}
|
|
184
|
+
const label = groupPresentationFor(props, row.billingClass)?.label;
|
|
185
|
+
return label === undefined ? scoped : { ...scoped, billingClassLabel: label };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type ModelPickerMenuGroup = {
|
|
190
|
+
billingClass: PickerBillingClass;
|
|
191
|
+
label: string;
|
|
192
|
+
description: string | null;
|
|
193
|
+
rows: ClientPickerModelRow[];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type ModelPickerThinking = {
|
|
197
|
+
efforts: ReasoningEffort[];
|
|
198
|
+
value: ReasoningEffort;
|
|
199
|
+
showThinking: boolean;
|
|
200
|
+
showFast: boolean;
|
|
201
|
+
/** The selected row cannot run, so its controls are read-only. */
|
|
202
|
+
disabled: boolean;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type ModelPickerMenuView = {
|
|
206
|
+
messages: ModelPolicyPickerMessages;
|
|
207
|
+
rows: ClientPickerModelRow[];
|
|
208
|
+
selected: ClientPickerModelRow | null;
|
|
209
|
+
/** Any row can run; otherwise the surface offers to connect models instead. */
|
|
210
|
+
anySelectable: boolean;
|
|
211
|
+
/** Whether the query is non-empty (decides "no matches" versus "no models"). */
|
|
212
|
+
searching: boolean;
|
|
213
|
+
groups: ModelPickerMenuGroup[];
|
|
214
|
+
matchCount: number;
|
|
215
|
+
/** Thinking and Fast controls for the selected model, or null when it has none. */
|
|
216
|
+
thinking: ModelPickerThinking | null;
|
|
217
|
+
/** The selected model cannot view images the draft carries. */
|
|
218
|
+
unsupportedAttachments: boolean;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** Everything a picker menu shows for this input and search query. */
|
|
222
|
+
export function modelPickerMenuView<TIcon>(
|
|
223
|
+
props: ModelPickerInput<TIcon>,
|
|
224
|
+
query: string,
|
|
225
|
+
): ModelPickerMenuView {
|
|
226
|
+
const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
|
|
227
|
+
const rows = effectiveRows(props);
|
|
228
|
+
const selected = findPickerRow(rows, props.model);
|
|
229
|
+
const words = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
230
|
+
const filtered = rows.filter((row) => {
|
|
231
|
+
const description = groupPresentationFor(props, row.billingClass)?.description;
|
|
232
|
+
const text =
|
|
233
|
+
`${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${description === undefined ? payerSummaryForModel(row.catalog) : (description ?? "")}`.toLowerCase();
|
|
234
|
+
return words.every((word) => text.includes(word));
|
|
235
|
+
});
|
|
236
|
+
const matchingIds = new Set(filtered.map((row) => row.id));
|
|
237
|
+
const groups = groupPickerRowsByBillingClass(rows, {
|
|
238
|
+
codexOnly: props.codexOnly === true,
|
|
239
|
+
selectedId: props.model,
|
|
240
|
+
collapseScopes: props.collapseScopes !== false,
|
|
241
|
+
})
|
|
242
|
+
.map((group) => {
|
|
243
|
+
const presentation = groupPresentationFor(props, group.billingClass);
|
|
244
|
+
const override = presentation?.description;
|
|
245
|
+
const defaultHint =
|
|
246
|
+
props.collapseScopes === false &&
|
|
247
|
+
messages.billingHints[group.billingClass] ===
|
|
248
|
+
defaultModelPolicyPickerMessages.billingHints[group.billingClass]
|
|
249
|
+
? (SCOPED_BILLING_HINTS[group.billingClass] ?? messages.billingHints[group.billingClass])
|
|
250
|
+
: messages.billingHints[group.billingClass];
|
|
251
|
+
return {
|
|
252
|
+
billingClass: group.billingClass,
|
|
253
|
+
label: presentation?.label ?? group.label,
|
|
254
|
+
rows: group.rows.filter((row) => matchingIds.has(row.id)),
|
|
255
|
+
description:
|
|
256
|
+
override === undefined
|
|
257
|
+
? group.billingClass === "opengeni_credits"
|
|
258
|
+
? null
|
|
259
|
+
: defaultHint
|
|
260
|
+
: override,
|
|
261
|
+
};
|
|
262
|
+
})
|
|
263
|
+
.filter((group) => group.rows.length > 0);
|
|
264
|
+
const anySelectable = rows.some((row) => row.selectable);
|
|
265
|
+
const unsupportedAttachments =
|
|
266
|
+
props.hasImageAttachments === true &&
|
|
267
|
+
selected?.catalog.capabilities?.inputModalities.includes("image") === false;
|
|
268
|
+
return {
|
|
269
|
+
messages,
|
|
270
|
+
rows,
|
|
271
|
+
selected,
|
|
272
|
+
anySelectable,
|
|
273
|
+
searching: words.length > 0,
|
|
274
|
+
groups,
|
|
275
|
+
matchCount: filtered.length,
|
|
276
|
+
thinking: selected ? modelPickerThinking(props, selected) : null,
|
|
277
|
+
unsupportedAttachments,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function modelPickerThinking(
|
|
282
|
+
props: Pick<ModelPickerInput, "effort" | "allowLatencyMode">,
|
|
283
|
+
selected: ClientPickerModelRow,
|
|
284
|
+
): ModelPickerThinking | null {
|
|
285
|
+
const efforts = effortOptionsForModel(selected.catalog);
|
|
286
|
+
const showThinking =
|
|
287
|
+
efforts.length > 1 && selected.catalog.capabilities?.reasoning.runnable !== false;
|
|
288
|
+
const showFast =
|
|
289
|
+
props.allowLatencyMode !== false &&
|
|
290
|
+
runnableLatencyModesForModel(selected.catalog).includes("fast");
|
|
291
|
+
if (!showThinking && !showFast) return null;
|
|
292
|
+
return {
|
|
293
|
+
efforts,
|
|
294
|
+
value: coerceReasoningEffortForModel(selected.catalog, props.effort),
|
|
295
|
+
showThinking,
|
|
296
|
+
showFast,
|
|
297
|
+
disabled: !selected.selectable,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export type ModelPickerChoice = {
|
|
302
|
+
/** The new model, or null when the row is already selected. */
|
|
303
|
+
model: string | null;
|
|
304
|
+
/** Always reported, so hosts can commit model and effort together. */
|
|
305
|
+
effort: ReasoningEffort;
|
|
306
|
+
/** Fast falls back to standard when the new model cannot run it. */
|
|
307
|
+
latencyMode: LatencyMode | null;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/** What choosing a row changes, or null when the row cannot be chosen. */
|
|
311
|
+
export function modelPickerChoice(
|
|
312
|
+
props: Pick<ModelPickerInput, "model" | "effort" | "latencyMode">,
|
|
313
|
+
row: ClientPickerModelRow,
|
|
314
|
+
): ModelPickerChoice | null {
|
|
315
|
+
if (!row.selectable) return null;
|
|
316
|
+
return {
|
|
317
|
+
model: row.id === props.model ? null : row.id,
|
|
318
|
+
effort: coerceReasoningEffortForModel(row.catalog, props.effort),
|
|
319
|
+
latencyMode:
|
|
320
|
+
props.latencyMode !== "standard" &&
|
|
321
|
+
!runnableLatencyModesForModel(row.catalog).includes(props.latencyMode)
|
|
322
|
+
? "standard"
|
|
323
|
+
: null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The web segmented control's short labels ("X-high" fits a phone row). */
|
|
328
|
+
export function compactEffortLabel(effort: ReasoningEffort, label: string): string {
|
|
329
|
+
return effort === "xhigh" ? "X-high" : label;
|
|
330
|
+
}
|