@rol1n/pi-kit 0.0.0-stage → 0.1.1

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.
Files changed (120) hide show
  1. package/README.md +73 -2
  2. package/ask-u-questions/README.md +26 -0
  3. package/ask-u-questions/dialog-ui.ts +80 -0
  4. package/ask-u-questions/index.ts +92 -0
  5. package/ask-u-questions/multi-choice.ts +258 -0
  6. package/ask-u-questions/single-choice.ts +180 -0
  7. package/ask-u-questions/tool-renderers.ts +67 -0
  8. package/ask-u-questions/types.ts +192 -0
  9. package/continue-signal/README.md +20 -0
  10. package/continue-signal/index.ts +69 -0
  11. package/custom-providers/README.md +20 -0
  12. package/custom-providers/asr/handlers/dashscope.ts +154 -0
  13. package/custom-providers/asr/index.ts +37 -0
  14. package/custom-providers/asr/models.ts +18 -0
  15. package/custom-providers/asr/types.ts +20 -0
  16. package/custom-providers/chat/index.ts +33 -0
  17. package/custom-providers/chat/magpie.ts +258 -0
  18. package/custom-providers/chat/models.ts +111 -0
  19. package/custom-providers/classifier/index.ts +6 -0
  20. package/custom-providers/classifier/models.ts +1 -0
  21. package/custom-providers/image/handlers/dashscope.ts +240 -0
  22. package/custom-providers/image/handlers/openrouter.ts +185 -0
  23. package/custom-providers/image/index.ts +96 -0
  24. package/custom-providers/image/models.ts +71 -0
  25. package/custom-providers/image/types.ts +22 -0
  26. package/custom-providers/index.ts +64 -0
  27. package/custom-providers/types.ts +17 -0
  28. package/image-generate/README.md +25 -0
  29. package/image-generate/files.ts +49 -0
  30. package/image-generate/index.ts +220 -0
  31. package/image-generate/model-selector.ts +150 -0
  32. package/memory-log/README.md +54 -0
  33. package/memory-log/agent/consolidator/prompt.ts +47 -0
  34. package/memory-log/agent/consolidator/tools.ts +160 -0
  35. package/memory-log/agent/cost.ts +32 -0
  36. package/memory-log/agent/index.ts +42 -0
  37. package/memory-log/agent/observer/prompt.ts +73 -0
  38. package/memory-log/agent/observer/tool.ts +73 -0
  39. package/memory-log/commands/compact.ts +35 -0
  40. package/memory-log/commands/consolidate.ts +47 -0
  41. package/memory-log/commands/status.ts +50 -0
  42. package/memory-log/config.ts +253 -0
  43. package/memory-log/debug-log.ts +76 -0
  44. package/memory-log/hooks/compaction-hook.ts +161 -0
  45. package/memory-log/hooks/compaction-trigger.ts +160 -0
  46. package/memory-log/hooks/consolidator-trigger.ts +143 -0
  47. package/memory-log/hooks/observer-trigger.ts +176 -0
  48. package/memory-log/ids.ts +78 -0
  49. package/memory-log/index.ts +149 -0
  50. package/memory-log/ledger/fold.ts +77 -0
  51. package/memory-log/ledger/index.ts +7 -0
  52. package/memory-log/ledger/pool.ts +38 -0
  53. package/memory-log/ledger/progress.ts +181 -0
  54. package/memory-log/ledger/projection.ts +115 -0
  55. package/memory-log/ledger/render.ts +38 -0
  56. package/memory-log/ledger/serialize.ts +161 -0
  57. package/memory-log/ledger/types.ts +201 -0
  58. package/memory-log/memory/index-render.ts +45 -0
  59. package/memory-log/memory/paths.ts +120 -0
  60. package/memory-log/memory/session.ts +64 -0
  61. package/memory-log/runtime.ts +127 -0
  62. package/memory-log/spawn/launch.ts +133 -0
  63. package/memory-log/spawn/runs.ts +83 -0
  64. package/memory-log/tokens.ts +66 -0
  65. package/memory-log/ui/status-controller.ts +225 -0
  66. package/memory-log/ui/timeline.ts +103 -0
  67. package/model-vision/README.md +22 -0
  68. package/multi-harness/README.md +63 -0
  69. package/multi-harness/adapters.mjs +72 -0
  70. package/multi-harness/harness-done.mjs +14 -0
  71. package/multi-harness/index.ts +375 -0
  72. package/multi-harness/ipc.mjs +37 -0
  73. package/multi-harness/opencode-plugin.mjs +99 -0
  74. package/multi-harness/process-runner.mjs +115 -0
  75. package/multi-harness/turn-hook.mjs +29 -0
  76. package/multi-harness/worker-bridge.ts +52 -0
  77. package/oai-subquota/README.md +22 -0
  78. package/oai-subquota/index.ts +286 -0
  79. package/oai-subquota/snapshot.ts +293 -0
  80. package/oai-subquota/ui.ts +183 -0
  81. package/package.json +92 -4
  82. package/prompt-snipaste/README.md +27 -0
  83. package/prompt-snipaste/index.ts +143 -0
  84. package/prompt-snipaste/menu.ts +159 -0
  85. package/prompt-snipaste/prompts/ask-questions.md +8 -0
  86. package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
  87. package/prompt-snipaste/prompts/diagnose-report.md +7 -0
  88. package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
  89. package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
  90. package/prompt-snipaste/prompts/session-kickoff.md +7 -0
  91. package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
  92. package/prompt-snipaste/renderers.ts +87 -0
  93. package/prompt-snipaste/snippets.ts +206 -0
  94. package/prompt-snipaste/types.ts +18 -0
  95. package/screenshot/README.md +14 -0
  96. package/srp-theme/README.md +18 -0
  97. package/srp-theme/footer.ts +193 -0
  98. package/srp-theme/index.ts +172 -0
  99. package/srp-theme/tps-meter.ts +441 -0
  100. package/subagent/README.md +35 -0
  101. package/subagent/bridge.ts +171 -0
  102. package/subagent/index.ts +590 -0
  103. package/subagent/mux.ts +621 -0
  104. package/subagent/session.ts +166 -0
  105. package/subagent/ui.ts +100 -0
  106. package/tui-asr/README.md +21 -0
  107. package/tui-asr/dictation.ts +272 -0
  108. package/tui-asr/index.ts +181 -0
  109. package/tui-asr/input.ts +87 -0
  110. package/tui-asr/model-selector.ts +163 -0
  111. package/tui-asr/status.ts +79 -0
  112. package/web-reach/README.md +19 -0
  113. package/web-reach/html.ts +83 -0
  114. package/web-reach/index.ts +82 -0
  115. package/web-reach/page.ts +214 -0
  116. package/web-reach/rsc.ts +161 -0
  117. package/web-reach/search.ts +85 -0
  118. package/web-reach/transport.ts +35 -0
  119. package/windows-wheel-fix/README.md +12 -0
  120. package/windows-wheel-fix/index.ts +44 -0
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { StreamingAsrProvider, VoiceCallbacks } from "../types.ts";
3
+
4
+ export class DashscopeAsrProvider implements StreamingAsrProvider {
5
+ private ws: WebSocket | null = null;
6
+ private taskId: string = "";
7
+ private lastSentenceMap = new Map<number, string>();
8
+ private isCancelled = false;
9
+
10
+ constructor(
11
+ private model: string,
12
+ private apiKey: string,
13
+ ) {}
14
+
15
+ async start(callbacks: VoiceCallbacks): Promise<void> {
16
+ this.isCancelled = false;
17
+ if (!this.apiKey) {
18
+ throw new Error("DashScope API Key not found. Please run /login dashscope or set DASHSCOPE_API_KEY environment variable.");
19
+ }
20
+
21
+ this.taskId = randomUUID().replace(/-/g, "");
22
+ this.lastSentenceMap.clear();
23
+
24
+ const url = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/";
25
+ const ws = new WebSocket(url, {
26
+ headers: {
27
+ Authorization: `Bearer ${this.apiKey}`,
28
+ },
29
+ } as any);
30
+ this.ws = ws;
31
+
32
+ return new Promise((resolve, reject) => {
33
+ let taskStarted = false;
34
+
35
+ ws.addEventListener("open", () => {
36
+ if (this.isCancelled) return;
37
+ const startMsg = {
38
+ header: {
39
+ action: "run-task",
40
+ task_id: this.taskId,
41
+ streaming: "duplex",
42
+ },
43
+ payload: {
44
+ task_group: "audio",
45
+ task: "asr",
46
+ function: "recognition",
47
+ model: this.model,
48
+ parameters: {
49
+ format: "pcm",
50
+ sample_rate: 16000,
51
+ },
52
+ input: {},
53
+ },
54
+ };
55
+ try {
56
+ ws.send(JSON.stringify(startMsg));
57
+ } catch {}
58
+ });
59
+
60
+ ws.addEventListener("message", (ev) => {
61
+ if (this.isCancelled) return;
62
+ try {
63
+ const msg = JSON.parse(ev.data as string);
64
+ const header = msg.header;
65
+ if (header?.event === "task-started") {
66
+ taskStarted = true;
67
+ resolve();
68
+ } else if (header?.event === "result-generated") {
69
+ const sentences = msg.payload?.output?.sentence;
70
+ if (Array.isArray(sentences)) {
71
+ for (let i = 0; i < sentences.length; i++) {
72
+ const s = sentences[i];
73
+ if (s?.text && s.end_time !== undefined) {
74
+ if (!this.lastSentenceMap.has(i)) {
75
+ this.lastSentenceMap.set(i, s.text);
76
+ callbacks.onFinal(s.text.trim());
77
+ }
78
+ }
79
+ }
80
+ }
81
+ } else if (header?.event === "task-finished") {
82
+ const sentences = msg.payload?.output?.sentence;
83
+ if (Array.isArray(sentences)) {
84
+ for (let i = 0; i < sentences.length; i++) {
85
+ const s = sentences[i];
86
+ if (s?.text && !this.lastSentenceMap.has(i)) {
87
+ this.lastSentenceMap.set(i, s.text);
88
+ callbacks.onFinal(s.text.trim());
89
+ }
90
+ }
91
+ }
92
+ callbacks.onClose();
93
+ } else if (header?.event === "task-failed") {
94
+ const err = new Error(header.error_message || "DashScope ASR task failed");
95
+ if (!taskStarted) reject(err);
96
+ else callbacks.onError(err);
97
+ }
98
+ } catch {}
99
+ });
100
+
101
+ ws.addEventListener("error", () => {
102
+ if (this.isCancelled) return;
103
+ const err = new Error("DashScope WebSocket connection error");
104
+ if (!taskStarted) reject(err);
105
+ else callbacks.onError(err);
106
+ });
107
+
108
+ ws.addEventListener("close", () => {
109
+ if (this.isCancelled) return;
110
+ callbacks.onClose();
111
+ });
112
+ });
113
+ }
114
+
115
+ sendAudio(chunk: Buffer): void {
116
+ if (!this.isCancelled && this.ws && this.ws.readyState === WebSocket.OPEN) {
117
+ this.ws.send(chunk);
118
+ }
119
+ }
120
+
121
+ async stop(): Promise<void> {
122
+ if (!this.isCancelled && this.ws && this.ws.readyState === WebSocket.OPEN && this.taskId) {
123
+ try {
124
+ const finishMsg = {
125
+ header: {
126
+ action: "finish-task",
127
+ task_id: this.taskId,
128
+ streaming: "duplex",
129
+ },
130
+ payload: {
131
+ input: {},
132
+ },
133
+ };
134
+ this.ws.send(JSON.stringify(finishMsg));
135
+ } catch {}
136
+ }
137
+ }
138
+
139
+ cancel(): void {
140
+ this.isCancelled = true;
141
+ if (this.ws) {
142
+ try {
143
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
144
+ this.ws.close();
145
+ }
146
+ } catch {}
147
+ this.ws = null;
148
+ }
149
+ }
150
+ }
151
+
152
+ export function createDashscopeAsrProvider(modelId: string, apiKey: string): StreamingAsrProvider {
153
+ return new DashscopeAsrProvider(modelId, apiKey);
154
+ }
@@ -0,0 +1,37 @@
1
+ import { DASHSCOPE_ASR_MODELS } from "./models.ts";
2
+ import { createDashscopeAsrProvider } from "./handlers/dashscope.ts";
3
+ import type { AsrModelDefinition, StreamingAsrProvider } from "./types.ts";
4
+
5
+ export * from "./types.ts";
6
+ export * from "./models.ts";
7
+ export * from "./handlers/dashscope.ts";
8
+
9
+ export interface AsrProviderConfig {
10
+ name: string;
11
+ models: AsrModelDefinition[];
12
+ createProvider: (modelId: string, apiKey: string) => StreamingAsrProvider;
13
+ }
14
+
15
+ export const asrProviders: Record<string, AsrProviderConfig> = {
16
+ dashscope: {
17
+ name: "Aliyun DashScope",
18
+ models: DASHSCOPE_ASR_MODELS,
19
+ createProvider: createDashscopeAsrProvider,
20
+ },
21
+ };
22
+
23
+ export const ALL_ASR_MODELS: AsrModelDefinition[] = [
24
+ ...DASHSCOPE_ASR_MODELS,
25
+ ];
26
+
27
+ export function createAsrProvider(
28
+ providerId: string,
29
+ modelId: string,
30
+ apiKey: string,
31
+ ): StreamingAsrProvider {
32
+ const provider = asrProviders[providerId];
33
+ if (!provider) {
34
+ throw new Error(`Unsupported ASR provider: ${providerId}`);
35
+ }
36
+ return provider.createProvider(modelId, apiKey);
37
+ }
@@ -0,0 +1,18 @@
1
+ import type { AsrModelDefinition } from "./types.ts";
2
+
3
+ export const DASHSCOPE_ASR_MODELS: AsrModelDefinition[] = [
4
+ {
5
+ id: "paraformer-realtime-v2",
6
+ name: "Paraformer Realtime v2",
7
+ provider: "dashscope",
8
+ description: "Real-time speech recognition with high-accuracy punctuation and sentence segmentation (recommended).",
9
+ sampleRate: 16000,
10
+ },
11
+ {
12
+ id: "paraformer-8k-realtime-v1",
13
+ name: "Paraformer 8k Realtime v1",
14
+ provider: "dashscope",
15
+ description: "8k narrowband telephony real-time speech recognition.",
16
+ sampleRate: 8000,
17
+ },
18
+ ];
@@ -0,0 +1,20 @@
1
+ export interface AsrModelDefinition {
2
+ id: string;
3
+ name: string;
4
+ provider: string;
5
+ description: string;
6
+ sampleRate?: number;
7
+ }
8
+
9
+ export interface VoiceCallbacks {
10
+ onFinal(text: string): void;
11
+ onError(err: Error): void;
12
+ onClose(): void;
13
+ }
14
+
15
+ export interface StreamingAsrProvider {
16
+ start(callbacks: VoiceCallbacks): Promise<void>;
17
+ sendAudio(chunk: Buffer): void;
18
+ stop(): Promise<void>;
19
+ cancel(): void;
20
+ }
@@ -0,0 +1,33 @@
1
+ import type { ModalityProvidersRecord } from "../types.ts";
2
+ import { magpieChatProvider } from "./magpie.ts";
3
+ import {
4
+ SHUAIAPI_MODELS,
5
+ BIGMODEL_MODELS,
6
+ DASHSCOPE_CHAT_MODELS,
7
+ } from "./models.ts";
8
+
9
+ const SHUAIAPI_BASE_URL = "https://cdn.shuaiapi.com/v1";
10
+ const BIGMODEL_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
11
+ const DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
12
+
13
+ export const chatProviders: ModalityProvidersRecord = {
14
+ magpie: magpieChatProvider,
15
+ shuaiapi: {
16
+ name: "SHUAI API",
17
+ baseUrl: SHUAIAPI_BASE_URL,
18
+ api: "openai-completions",
19
+ models: SHUAIAPI_MODELS,
20
+ },
21
+ bigmodel: {
22
+ name: "BigModel (智谱)",
23
+ baseUrl: BIGMODEL_BASE_URL,
24
+ api: "openai-completions",
25
+ models: BIGMODEL_MODELS,
26
+ },
27
+ dashscope: {
28
+ name: "Aliyun DashScope (阿里百炼)",
29
+ baseUrl: DASHSCOPE_BASE_URL,
30
+ api: "openai-completions",
31
+ models: DASHSCOPE_CHAT_MODELS,
32
+ },
33
+ };
@@ -0,0 +1,258 @@
1
+ import type { AnyModel, Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ProviderConfig, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
3
+
4
+ type ChatModelConfig = Extract<ProviderModelConfig, { type?: "chat" }>;
5
+ type ClassifierModelConfig = Extract<ProviderModelConfig, { type: "classifier" }>;
6
+ type RefreshContext = Parameters<NonNullable<ProviderConfig["refreshModels"]>>[0];
7
+
8
+ type MagpieChatModelConfig = ChatModelConfig & {
9
+ api: "anthropic-messages" | "google-generative-ai" | "openai-responses" | "openai-completions";
10
+ baseUrl: string;
11
+ };
12
+
13
+ type MagpieClassifierModelConfig = ClassifierModelConfig & {
14
+ type: "classifier";
15
+ api: "typesafe-system-one";
16
+ baseUrl: string;
17
+ };
18
+
19
+ type MagpieDiscoveredModelConfig = MagpieChatModelConfig | MagpieClassifierModelConfig;
20
+
21
+ // Authenticate with a Magpie gateway key via /login magpie (stored in Pi's
22
+ // auth.json) or MAGPIE_GATEWAY_KEY. This is not an upstream provider API key.
23
+
24
+ const BASE_URL = process.env.MAGPIE_BASE_URL ?? "http://127.0.0.1:3425/v1";
25
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
26
+ const DEFAULT_MAX_TOKENS = 8_192;
27
+
28
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
29
+ const NON_CHAT_TYPES = new Set(["image", "video", "audio", "embedding", "rerank"]);
30
+
31
+ // Follow Magpie's own Pi adapter and gateway's ThinksOnlyWhenAsked /
32
+ // AdaptiveThinking predicates, not a blanket assumption about Messages models:
33
+ // internal/agent/agents.go and internal/gateway/anthropic.go (dee5c9d).
34
+ const CLAUDE_MODEL = /(?:^|[/.:-])claude-/i;
35
+ const CLAUDE_VERSION = /(?:^|[^a-z0-9])(?:claude-)?(?:opus|sonnet|haiku|fable|mythos)-(\d{1,2})(?:[-.](\d{1,2}))?(?:[^0-9]|$)|claude-(\d+)(?:[-.](\d))?-(?:opus|sonnet|haiku)/i;
36
+ const GEMINI_MODEL = /(?:^|\/)(?:gemini|gemma)[-.]/i;
37
+ const DECISION_MODEL = /(?:^|\/)(?:jev(?:-|$)|decision-model)/i;
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value);
41
+ }
42
+
43
+ function positiveInteger(value: unknown): number | undefined {
44
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
45
+ }
46
+
47
+ function reasoningLevels(entry: Record<string, unknown>): string[] {
48
+ const flat = entry.reasoning_levels;
49
+ const levels = Array.isArray(flat) && flat.every((level) => typeof level === "string")
50
+ ? flat
51
+ : Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [];
52
+ return levels.flatMap((level) => {
53
+ const effort = isRecord(level) ? level.effort : level;
54
+ return typeof effort === "string" && effort.trim() ? [effort.trim().toLowerCase()] : [];
55
+ });
56
+ }
57
+
58
+ function thinkingLevelMap(levels: string[], claudeOff: boolean): ChatModelConfig["thinkingLevelMap"] {
59
+ // An absent/unknown effort list is not an exhaustive list of unsupported levels
60
+ // (e.g. budget-based thinking). Let Pi's native adapter supply its defaults.
61
+ if (!levels.some((level) => level === "none" || THINKING_LEVELS.some((known) => known !== "off" && known === level))) {
62
+ return undefined;
63
+ }
64
+ const mapping: NonNullable<ChatModelConfig["thinkingLevelMap"]> = Object.fromEntries(THINKING_LEVELS.map((level) => [
65
+ level,
66
+ level === "off" ? (levels.includes("none") ? "none" : null) : levels.includes(level) ? level : null,
67
+ ]));
68
+ // Claude can take thinking.type=disabled even when none is not an effort.
69
+ // Other Messages models (e.g. always-thinking GLM) must not inherit this.
70
+ if (claudeOff && mapping.off === null) delete mapping.off;
71
+ return mapping;
72
+ }
73
+
74
+ function toClassifierModel(entry: Record<string, unknown> & { id: string }, gatewayUrl: string): MagpieClassifierModelConfig {
75
+ return {
76
+ type: "classifier",
77
+ id: entry.id,
78
+ name: typeof entry.display_name === "string" && entry.display_name.trim() ? entry.display_name : entry.id,
79
+ api: "typesafe-system-one",
80
+ // TypeSafe System One transport appends /systemone to baseUrl.
81
+ // Magpie serves POST /v1/systemone, so baseUrl is `${gatewayUrl}/v1`.
82
+ baseUrl: `${gatewayUrl}/v1`,
83
+ contextWindow: positiveInteger(entry.context_window)
84
+ ?? positiveInteger(entry.context_length)
85
+ ?? positiveInteger(entry.max_input_tokens)
86
+ ?? 64_000,
87
+ input: ["text"],
88
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
89
+ };
90
+ }
91
+
92
+ function isDecisionModel(entry: Record<string, unknown>, endpoints: string[]): boolean {
93
+ if (entry.kind === "decision" || entry.type === "classifier") return true;
94
+ if (endpoints.some((ep) => ep.includes("/v1/systemone"))) return true;
95
+ if (typeof entry.id === "string" && (DECISION_MODEL.test(entry.id) || entry.id.startsWith("group/jev"))) return true;
96
+ return false;
97
+ }
98
+
99
+ function toChatModel(entry: Record<string, unknown> & { id: string }, gatewayUrl: string): MagpieChatModelConfig {
100
+ // Follow Magpie's /v1/models metadata, not model-name/vendor heuristics:
101
+ // https://github.com/yetone/magpie/blob/main/docs/integrating.md
102
+ // Protocol precedence: Responses, then Messages, then native Google Gemini, then Chat fallback.
103
+ const endpoints = Array.isArray(entry.native_endpoints)
104
+ ? entry.native_endpoints.filter((ep): ep is string => typeof ep === "string")
105
+ : [];
106
+
107
+ const isGemini = endpoints.some((ep) => ep.includes("/v1beta") || ep.includes("generateContent")) ||
108
+ ((typeof entry.owned_by === "string" && ["google", "google-vertex", "gemini"].includes(entry.owned_by.toLowerCase())) ||
109
+ GEMINI_MODEL.test(entry.id)) && !endpoints.includes("/v1/responses") && !endpoints.includes("/v1/messages");
110
+
111
+ const api: MagpieChatModelConfig["api"] = endpoints.includes("/v1/responses") ? "openai-responses"
112
+ : endpoints.includes("/v1/messages") ? "anthropic-messages"
113
+ : isGemini ? "google-generative-ai"
114
+ : "openai-completions";
115
+
116
+ const version = api === "anthropic-messages" ? CLAUDE_VERSION.exec(entry.id) : null;
117
+ const claudeOff = api === "anthropic-messages" && (CLAUDE_MODEL.test(entry.id) || version !== null);
118
+ const major = Number(version?.[3] ?? version?.[1] ?? 0);
119
+ const minor = Number(version?.[4] ?? version?.[2] ?? 0);
120
+ const adaptive = major > 4 || (major === 4 && minor >= 6);
121
+ const levels = reasoningLevels(entry);
122
+ const reasoning = typeof entry.reasoning === "boolean" ? entry.reasoning : levels.length > 0;
123
+ const effortMap = reasoning ? thinkingLevelMap(levels, claudeOff) : undefined;
124
+ const modalities = isRecord(entry.modalities) ? entry.modalities.input : undefined;
125
+ const input: ChatModelConfig["input"] = Array.isArray(modalities)
126
+ ? [...new Set(modalities.filter((value): value is "text" | "image" => value === "text" || value === "image"))]
127
+ : [];
128
+ const contextWindow = positiveInteger(entry.context_window)
129
+ ?? positiveInteger(entry.context_length)
130
+ ?? positiveInteger(entry.max_input_tokens)
131
+ ?? DEFAULT_CONTEXT_WINDOW;
132
+
133
+ // Base URL selection:
134
+ // - Anthropic: gatewayUrl (Anthropic SDK appends /v1/messages)
135
+ // - Gemini: `${gatewayUrl}/v1beta` (Google GenAI SDK appends /models/{model}:streamGenerateContent)
136
+ // - OpenAI: `${gatewayUrl}/v1` (OpenAI SDK appends /responses or /chat/completions)
137
+ const baseUrl = api === "anthropic-messages" ? gatewayUrl
138
+ : api === "google-generative-ai" ? `${gatewayUrl}/v1beta`
139
+ : `${gatewayUrl}/v1`;
140
+
141
+ return {
142
+ type: "chat",
143
+ id: entry.id,
144
+ name: typeof entry.display_name === "string" && entry.display_name.trim() ? entry.display_name : entry.id,
145
+ api,
146
+ baseUrl,
147
+ reasoning,
148
+ ...(effortMap ? { thinkingLevelMap: effortMap } : {}),
149
+ // Only recognized Claude versions use Magpie's adaptive policy. Effort
150
+ // names alone do not prove support; opaque aliases need modelOverrides.
151
+ ...(adaptive ? { compat: { forceAdaptiveThinking: true } } : {}),
152
+ input: input.length > 0 ? input : ["text"],
153
+ contextWindow,
154
+ maxTokens: Math.min(positiveInteger(entry.max_output_tokens) ?? DEFAULT_MAX_TOKENS, contextWindow),
155
+ // Magpie does not publish prices here. Zero means unknown, not free;
156
+ // modelOverrides in models.json can provide verified prices and capabilities.
157
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
158
+ };
159
+ }
160
+
161
+ export const refreshMagpieModels = async (context: RefreshContext): Promise<ProviderModelConfig[]> => {
162
+ const gatewayUrl = (context.baseUrl ?? BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
163
+ // Keep the entire snapshot: rebuilding from IDs would silently turn native
164
+ // Messages/Responses/Gemini/System One models back into Chat and discard capabilities.
165
+ // Reject snapshots from another gateway, including during offline startup:
166
+ // otherwise a changed gateway/key could send future prompts to the old host.
167
+ const cached = (context.stored?.models ?? [])
168
+ .filter((model): model is AnyModel => model.provider === "magpie")
169
+ .filter((model) => {
170
+ const type = model.type ?? "chat";
171
+ if (type === "classifier") {
172
+ return model.api === "typesafe-system-one" && model.baseUrl.replace(/\/+$/, "") === `${gatewayUrl}/v1`;
173
+ }
174
+ if (type === "chat") {
175
+ const expectedUrl = model.api === "anthropic-messages" ? gatewayUrl
176
+ : model.api === "google-generative-ai" ? `${gatewayUrl}/v1beta`
177
+ : model.api === "openai-responses" || model.api === "openai-completions" ? `${gatewayUrl}/v1` : undefined;
178
+ return Boolean(expectedUrl && model.baseUrl.replace(/\/+$/, "") === expectedUrl);
179
+ }
180
+ return false;
181
+ })
182
+ .map((model) => ({ ...model }));
183
+ if (!context.allowNetwork || context.signal.aborted) return cached;
184
+ const key = context.credential?.type === "api_key"
185
+ ? context.credential.key
186
+ : process.env.MAGPIE_GATEWAY_KEY;
187
+ if (!key) return cached;
188
+
189
+ // Magpie accepts a gateway key in Bearer, x-api-key, or ?key=; avoid URL/log leakage.
190
+ // Send X-Magpie-Deciders to include System One decision models, and X-Magpie-Prices for prices.
191
+ const response = await fetch(`${gatewayUrl}/v1/models`, {
192
+ headers: {
193
+ Authorization: `Bearer ${key}`,
194
+ "X-Magpie-Deciders": "1",
195
+ "X-Magpie-Prices": "1",
196
+ },
197
+ signal: context.signal,
198
+ });
199
+ if (!response.ok) throw new Error(`Magpie model discovery failed (HTTP ${response.status})`);
200
+ const catalog: unknown = await response.json();
201
+ if (!isRecord(catalog) || !Array.isArray(catalog.data)) {
202
+ throw new Error("Magpie model discovery returned an invalid /v1/models response");
203
+ }
204
+
205
+ const discovered = new Map<string, MagpieDiscoveredModelConfig>();
206
+ for (const entry of catalog.data) {
207
+ if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim()) continue;
208
+ if ((typeof entry.type === "string" && NON_CHAT_TYPES.has(entry.type)) || discovered.has(entry.id)) continue;
209
+ const endpoints = Array.isArray(entry.native_endpoints)
210
+ ? entry.native_endpoints.filter((ep): ep is string => typeof ep === "string")
211
+ : [];
212
+ if (isDecisionModel(entry, endpoints)) {
213
+ discovered.set(entry.id, toClassifierModel({ ...entry, id: entry.id }, gatewayUrl));
214
+ } else {
215
+ discovered.set(entry.id, toChatModel({ ...entry, id: entry.id }, gatewayUrl));
216
+ }
217
+ }
218
+ if (context.signal.aborted) return cached;
219
+ const models = [...discovered.values()];
220
+ await context.publish({
221
+ persist: {
222
+ checkedAt: Date.now(),
223
+ models: models.map((model) => ({
224
+ ...model,
225
+ provider: "magpie",
226
+ })),
227
+ },
228
+ });
229
+ return models;
230
+ };
231
+
232
+ async function loadTypesafeSystemOneClassifier() {
233
+ // Use the host's public provider collection so Pi can map the module in both
234
+ // npm and bundled installations. No machine-specific fallback path is needed.
235
+ const { builtinProviders } = await import("@earendil-works/pi-ai/providers/all");
236
+ const provider = builtinProviders().find((candidate) => candidate.id === "typesafe");
237
+ if (!provider?.classify) {
238
+ throw new Error("This Pi version does not provide the TypeSafe System One classifier");
239
+ }
240
+ return { classify: provider.classify };
241
+ }
242
+
243
+ export const magpieChatProvider = {
244
+ name: "Magpie",
245
+ baseUrl: BASE_URL,
246
+ api: "openai-completions",
247
+ apiKey: "$MAGPIE_GATEWAY_KEY",
248
+ models: [],
249
+ refreshModels: refreshMagpieModels,
250
+ classifiers: {
251
+ "typesafe-system-one": {
252
+ classify: async (model, context, options) => {
253
+ const impl = await loadTypesafeSystemOneClassifier();
254
+ return impl.classify(model, context, options);
255
+ },
256
+ },
257
+ },
258
+ } satisfies ProviderConfig;
@@ -0,0 +1,111 @@
1
+ export const SHUAIAPI_MODELS: Record<string, unknown>[] = [
2
+ {
3
+ id: "gpt-6-sol",
4
+ name: "GPT-6 Sol",
5
+ reasoning: true,
6
+ input: ["text", "image"],
7
+ contextWindow: 1_000_000,
8
+ maxTokens: 128_000,
9
+ thinkingLevelMap: {
10
+ off: "none", minimal: null, low: "low", medium: "medium",
11
+ high: "high", xhigh: "xhigh", max: "max",
12
+ },
13
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
14
+ compat: { supportsUsageInStreaming: true, supportsDeveloperRole: false },
15
+ },
16
+ {
17
+ id: "gpt-6-astra",
18
+ name: "GPT-6 Astra",
19
+ reasoning: true,
20
+ input: ["text", "image"],
21
+ contextWindow: 1_000_000,
22
+ maxTokens: 128_000,
23
+ thinkingLevelMap: {
24
+ off: "none", minimal: null, low: "low", medium: "medium",
25
+ high: "high", xhigh: "xhigh", max: "max",
26
+ },
27
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
28
+ compat: { supportsUsageInStreaming: true, supportsDeveloperRole: false },
29
+ },
30
+ {
31
+ id: "gpt-6-luna",
32
+ name: "GPT-6 Luna",
33
+ reasoning: true,
34
+ input: ["text", "image"],
35
+ contextWindow: 1_000_000,
36
+ maxTokens: 128_000,
37
+ thinkingLevelMap: {
38
+ off: "none", minimal: null, low: "low", medium: "medium",
39
+ high: "high", xhigh: "xhigh", max: "max",
40
+ },
41
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
42
+ compat: { supportsUsageInStreaming: true, supportsDeveloperRole: false },
43
+ },
44
+ ];
45
+
46
+ export const BIGMODEL_MODELS: Record<string, unknown>[] = [
47
+ {
48
+ id: "glm-5.3-flash",
49
+ name: "GLM-5.3 Flash",
50
+ reasoning: true,
51
+ input: ["text", "image"],
52
+ contextWindow: 1_000_000,
53
+ maxTokens: 128_000,
54
+ thinkingLevelMap: {
55
+ minimal: null, low: "low", medium: null,
56
+ high: "high", xhigh: null, max: "max",
57
+ },
58
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
59
+ compat: {
60
+ supportsUsageInStreaming: true,
61
+ supportsDeveloperRole: false,
62
+ supportsReasoningEffort: true,
63
+ thinkingFormat: "zai",
64
+ maxTokensField: "max_tokens",
65
+ },
66
+ },
67
+ {
68
+ id: "glm-5.3",
69
+ name: "GLM-5.3",
70
+ reasoning: true,
71
+ input: ["text"],
72
+ contextWindow: 1_000_000,
73
+ maxTokens: 128_000,
74
+ thinkingLevelMap: {
75
+ minimal: null, low: "low", medium: null,
76
+ high: "high", xhigh: null, max: "max",
77
+ },
78
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
79
+ compat: {
80
+ supportsUsageInStreaming: true,
81
+ supportsDeveloperRole: false,
82
+ supportsReasoningEffort: true,
83
+ thinkingFormat: "zai",
84
+ maxTokensField: "max_tokens",
85
+ },
86
+ },
87
+ ];
88
+
89
+ export const DASHSCOPE_CHAT_MODELS: Record<string, unknown>[] = [
90
+ {
91
+ id: "qwen3.8-max",
92
+ name: "Qwen 3.8 Max (通义千问)",
93
+ reasoning: true,
94
+ input: ["text", "image"],
95
+ contextWindow: 1_000_000,
96
+ maxTokens: 131_072,
97
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
98
+ compat: { supportsUsageInStreaming: true },
99
+ },
100
+ {
101
+ id: "qwen3.8-flash",
102
+ name: "Qwen 3.8 Flash (通义千问)",
103
+ reasoning: true,
104
+ input: ["text", "image"],
105
+ contextWindow: 1_000_000,
106
+ maxTokens: 131_072,
107
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
108
+ compat: { supportsUsageInStreaming: true },
109
+ },
110
+ // Pending: qwen-audio-3.1-asr-flash-streaming (ASR; Pi has no audio model type).
111
+ ];
@@ -0,0 +1,6 @@
1
+ import type { ModalityProvidersRecord } from "../types.ts";
2
+ import { CLASSIFIER_MODELS } from "./models.ts";
3
+
4
+ export const classifierProviders: ModalityProvidersRecord = {
5
+ // Reserved for classifier models (type: "classifier", classify handler)
6
+ };
@@ -0,0 +1 @@
1
+ export const CLASSIFIER_MODELS: Record<string, unknown>[] = [];