@valni/cli-darwin-x64 0.0.0-stage → 0.1.3

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 (183) hide show
  1. package/LICENSE +52 -0
  2. package/package.json +18 -5
  3. package/runtime/LICENSE +52 -0
  4. package/runtime/assets/clankolas.png +0 -0
  5. package/runtime/examples/extensions/auto-commit-on-exit.ts +49 -0
  6. package/runtime/examples/extensions/bash-spawn-hook.ts +30 -0
  7. package/runtime/examples/extensions/bookmark.ts +50 -0
  8. package/runtime/examples/extensions/border-status-editor.ts +145 -0
  9. package/runtime/examples/extensions/built-in-tool-renderer.ts +249 -0
  10. package/runtime/examples/extensions/claude-rules.ts +86 -0
  11. package/runtime/examples/extensions/commands.ts +72 -0
  12. package/runtime/examples/extensions/confirm-destructive.ts +59 -0
  13. package/runtime/examples/extensions/custom-footer.ts +64 -0
  14. package/runtime/examples/extensions/custom-header.ts +73 -0
  15. package/runtime/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  16. package/runtime/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  17. package/runtime/examples/extensions/custom-provider-anthropic/package.json +19 -0
  18. package/runtime/examples/extensions/dirty-repo-guard.ts +56 -0
  19. package/runtime/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  20. package/runtime/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  21. package/runtime/examples/extensions/doom-overlay/doom/build.sh +152 -0
  22. package/runtime/examples/extensions/doom-overlay/doom/doomgeneric_valni.c +72 -0
  23. package/runtime/examples/extensions/doom-overlay/doom-component.ts +132 -0
  24. package/runtime/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  25. package/runtime/examples/extensions/doom-overlay/doom-keys.ts +104 -0
  26. package/runtime/examples/extensions/doom-overlay/index.ts +74 -0
  27. package/runtime/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  28. package/runtime/examples/extensions/dynamic-resources/dynamic.json +78 -0
  29. package/runtime/examples/extensions/dynamic-resources/index.ts +13 -0
  30. package/runtime/examples/extensions/dynamic-tools.ts +74 -0
  31. package/runtime/examples/extensions/entry-renderer.ts +41 -0
  32. package/runtime/examples/extensions/event-bus.ts +43 -0
  33. package/runtime/examples/extensions/file-trigger.ts +41 -0
  34. package/runtime/examples/extensions/git-checkpoint.ts +53 -0
  35. package/runtime/examples/extensions/git-merge-and-resolve.ts +115 -0
  36. package/runtime/examples/extensions/github-issue-autocomplete.ts +185 -0
  37. package/runtime/examples/extensions/hello.ts +26 -0
  38. package/runtime/examples/extensions/hidden-thinking-label.ts +53 -0
  39. package/runtime/examples/extensions/inline-bash.ts +94 -0
  40. package/runtime/examples/extensions/input-transform-streaming.ts +39 -0
  41. package/runtime/examples/extensions/input-transform.ts +43 -0
  42. package/runtime/examples/extensions/interactive-shell.ts +196 -0
  43. package/runtime/examples/extensions/mac-system-theme.ts +47 -0
  44. package/runtime/examples/extensions/message-renderer.ts +59 -0
  45. package/runtime/examples/extensions/minimal-mode.ts +426 -0
  46. package/runtime/examples/extensions/modal-editor.ts +85 -0
  47. package/runtime/examples/extensions/model-status.ts +31 -0
  48. package/runtime/examples/extensions/notify.ts +55 -0
  49. package/runtime/examples/extensions/overlay-qa-tests.ts +1450 -0
  50. package/runtime/examples/extensions/overlay-test.ts +153 -0
  51. package/runtime/examples/extensions/permission-gate.ts +34 -0
  52. package/runtime/examples/extensions/pirate.ts +47 -0
  53. package/runtime/examples/extensions/plan-mode/index.ts +390 -0
  54. package/runtime/examples/extensions/plan-mode/utils.ts +168 -0
  55. package/runtime/examples/extensions/preset.ts +436 -0
  56. package/runtime/examples/extensions/project-trust.ts +64 -0
  57. package/runtime/examples/extensions/prompt-customizer.ts +97 -0
  58. package/runtime/examples/extensions/protected-paths.ts +30 -0
  59. package/runtime/examples/extensions/provider-payload.ts +18 -0
  60. package/runtime/examples/extensions/question.ts +278 -0
  61. package/runtime/examples/extensions/questionnaire.ts +440 -0
  62. package/runtime/examples/extensions/rainbow-editor.ts +88 -0
  63. package/runtime/examples/extensions/reload-runtime.ts +37 -0
  64. package/runtime/examples/extensions/rpc-demo.ts +118 -0
  65. package/runtime/examples/extensions/sandbox/index.ts +321 -0
  66. package/runtime/examples/extensions/sandbox/package-lock.json +92 -0
  67. package/runtime/examples/extensions/sandbox/package.json +19 -0
  68. package/runtime/examples/extensions/send-user-message.ts +97 -0
  69. package/runtime/examples/extensions/session-name.ts +27 -0
  70. package/runtime/examples/extensions/shutdown-command.ts +63 -0
  71. package/runtime/examples/extensions/snake.ts +343 -0
  72. package/runtime/examples/extensions/space-invaders.ts +560 -0
  73. package/runtime/examples/extensions/ssh.ts +220 -0
  74. package/runtime/examples/extensions/status-line.ts +32 -0
  75. package/runtime/examples/extensions/structured-output.ts +65 -0
  76. package/runtime/examples/extensions/subagent/agents.ts +126 -0
  77. package/runtime/examples/extensions/subagent/index.ts +1009 -0
  78. package/runtime/examples/extensions/system-prompt-header.ts +17 -0
  79. package/runtime/examples/extensions/tic-tac-toe.ts +1008 -0
  80. package/runtime/examples/extensions/timed-confirm.ts +70 -0
  81. package/runtime/examples/extensions/titlebar-spinner.ts +58 -0
  82. package/runtime/examples/extensions/todo.ts +297 -0
  83. package/runtime/examples/extensions/tool-override.ts +144 -0
  84. package/runtime/examples/extensions/tools.ts +146 -0
  85. package/runtime/examples/extensions/trigger-compact.ts +50 -0
  86. package/runtime/examples/extensions/truncated-tool.ts +195 -0
  87. package/runtime/examples/extensions/widget-placement.ts +9 -0
  88. package/runtime/examples/extensions/with-deps/index.ts +32 -0
  89. package/runtime/examples/extensions/with-deps/package-lock.json +31 -0
  90. package/runtime/examples/extensions/with-deps/package.json +22 -0
  91. package/runtime/examples/extensions/working-indicator.ts +123 -0
  92. package/runtime/examples/extensions/working-message-test.ts +25 -0
  93. package/runtime/examples/rpc-extension-ui.ts +632 -0
  94. package/runtime/examples/sdk/01-minimal.ts +26 -0
  95. package/runtime/examples/sdk/02-custom-model.ts +43 -0
  96. package/runtime/examples/sdk/03-custom-prompt.ts +70 -0
  97. package/runtime/examples/sdk/04-skills.ts +55 -0
  98. package/runtime/examples/sdk/05-tools.ts +48 -0
  99. package/runtime/examples/sdk/06-extensions.ts +94 -0
  100. package/runtime/examples/sdk/07-context-files.ts +42 -0
  101. package/runtime/examples/sdk/08-prompt-templates.ts +51 -0
  102. package/runtime/examples/sdk/09-api-keys-and-oauth.ts +34 -0
  103. package/runtime/examples/sdk/10-settings.ts +53 -0
  104. package/runtime/examples/sdk/11-sessions.ts +52 -0
  105. package/runtime/examples/sdk/12-full-control.ts +71 -0
  106. package/runtime/examples/sdk/13-session-runtime.ts +67 -0
  107. package/runtime/export-html/ansi-to-html.js +249 -0
  108. package/runtime/export-html/index.js +226 -0
  109. package/runtime/export-html/template.css +1066 -0
  110. package/runtime/export-html/template.html +55 -0
  111. package/runtime/export-html/template.js +1864 -0
  112. package/runtime/export-html/tool-renderer.js +108 -0
  113. package/runtime/export-html/vendor/highlight.min.js +1213 -0
  114. package/runtime/export-html/vendor/marked.min.js +78 -0
  115. package/runtime/extensions/pi-switchboard/.github/workflows/ci.yml +51 -0
  116. package/runtime/extensions/pi-switchboard/LICENSE +22 -0
  117. package/runtime/extensions/pi-switchboard/README.md +85 -0
  118. package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +183 -0
  119. package/runtime/extensions/pi-switchboard/extensions/switchboard/config.ts +97 -0
  120. package/runtime/extensions/pi-switchboard/extensions/switchboard/connections.ts +386 -0
  121. package/runtime/extensions/pi-switchboard/extensions/switchboard/constants.ts +12 -0
  122. package/runtime/extensions/pi-switchboard/extensions/switchboard/device-auth.ts +127 -0
  123. package/runtime/extensions/pi-switchboard/extensions/switchboard/envelope.ts +199 -0
  124. package/runtime/extensions/pi-switchboard/extensions/switchboard/errors.ts +180 -0
  125. package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +125 -0
  126. package/runtime/extensions/pi-switchboard/extensions/switchboard/participant.ts +17 -0
  127. package/runtime/extensions/pi-switchboard/extensions/switchboard/sessionStream.ts +131 -0
  128. package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +136 -0
  129. package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +16 -0
  130. package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +37 -0
  131. package/runtime/extensions/pi-switchboard/package.json +30 -0
  132. package/runtime/extensions/pi-switchboard/tsconfig.json +13 -0
  133. package/runtime/extensions/switchboard-local/.github/workflows/ci.yml +80 -0
  134. package/runtime/extensions/switchboard-local/.github/workflows/release.yml +46 -0
  135. package/runtime/extensions/switchboard-local/README.md +98 -0
  136. package/runtime/extensions/switchboard-local/extensions/local/engine.ts +28 -0
  137. package/runtime/extensions/switchboard-local/extensions/local/engines/gguf.ts +324 -0
  138. package/runtime/extensions/switchboard-local/extensions/local/engines/llamaBuild.ts +62 -0
  139. package/runtime/extensions/switchboard-local/extensions/local/engines/mlx.ts +245 -0
  140. package/runtime/extensions/switchboard-local/extensions/local/engines/select.ts +38 -0
  141. package/runtime/extensions/switchboard-local/extensions/local/engines/support.ts +151 -0
  142. package/runtime/extensions/switchboard-local/extensions/local/hf.ts +106 -0
  143. package/runtime/extensions/switchboard-local/extensions/local/index.ts +67 -0
  144. package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +602 -0
  145. package/runtime/extensions/switchboard-local/extensions/local/paths.ts +44 -0
  146. package/runtime/extensions/switchboard-local/extensions/local/provider.ts +57 -0
  147. package/runtime/extensions/switchboard-local/extensions/local/store.ts +82 -0
  148. package/runtime/extensions/switchboard-local/extensions/local/ui.ts +60 -0
  149. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/apiTypes.js +74 -0
  150. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/client.js +292 -0
  151. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/deviceAuth.js +155 -0
  152. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/errors.js +95 -0
  153. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/index.js +8 -0
  154. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/messages.js +253 -0
  155. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/models.js +19 -0
  156. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/native.js +886 -0
  157. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/wire.js +85 -0
  158. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/package.json +39 -0
  159. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/package.json +29 -0
  160. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/anthropic.ts +127 -0
  161. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/google.ts +111 -0
  162. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/index.ts +8 -0
  163. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.test.ts +25 -0
  164. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.ts +57 -0
  165. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiGeneric.ts +103 -0
  166. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiPro.ts +89 -0
  167. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/profile.ts +120 -0
  168. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.test.ts +82 -0
  169. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.ts +74 -0
  170. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.google.test.ts +137 -0
  171. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.openai.test.ts +241 -0
  172. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.test.ts +174 -0
  173. package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.ts +438 -0
  174. package/runtime/extensions/switchboard-local/package.json +34 -0
  175. package/runtime/extensions/switchboard-local/tsconfig.json +13 -0
  176. package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  177. package/runtime/package.json +98 -0
  178. package/runtime/photon_rs_bg.wasm +0 -0
  179. package/runtime/theme/dark.json +86 -0
  180. package/runtime/theme/light.json +85 -0
  181. package/runtime/theme/theme-schema.json +340 -0
  182. package/runtime/valni +4 -0
  183. package/README.md +0 -3
@@ -0,0 +1,174 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { AnthropicProfile, ProfileFinding } from './profile.js';
3
+ import { validateConfig } from './validateConfig.js';
4
+
5
+ const richModel: AnthropicProfile = {
6
+ kind: 'anthropic',
7
+ model: 'test-rich',
8
+ maxTokensCeiling: 64000,
9
+ temperature: { min: 0, max: 1 },
10
+ topP: { min: 0, max: 1 },
11
+ topK: true,
12
+ stopSequences: true,
13
+ tools: true,
14
+ toolChoice: ['auto', 'any', 'none', 'tool'],
15
+ thinking: { modes: ['adaptive', 'enabled', 'disabled'], budgetTokens: { min: 1024, max: 32000 } },
16
+ effort: ['low', 'medium', 'high'],
17
+ vision: true,
18
+ };
19
+
20
+ const reasoningModel: AnthropicProfile = {
21
+ kind: 'anthropic',
22
+ model: 'test-reasoning',
23
+ maxTokensCeiling: 32000,
24
+ temperature: false,
25
+ thinking: false,
26
+ effort: false,
27
+ vision: false,
28
+ };
29
+
30
+ const unknownModel: AnthropicProfile = {
31
+ kind: 'anthropic',
32
+ model: 'test-unknown',
33
+ };
34
+
35
+ const baseRequest = {
36
+ model: 'test-rich',
37
+ messages: [{ role: 'user', content: 'hi' }],
38
+ max_tokens: 1024,
39
+ };
40
+
41
+ function codesFor(findings: ProfileFinding[], field: string): string[] {
42
+ return findings.filter(finding => finding.field === field).map(finding => finding.code);
43
+ }
44
+
45
+ describe('validateConfig — clean requests produce no findings', () => {
46
+ it('accepts a request that only sets required fields', () => {
47
+ expect(validateConfig(richModel, baseRequest)).toEqual([]);
48
+ });
49
+
50
+ it('accepts every knob set to a value inside the profile', () => {
51
+ const request = {
52
+ ...baseRequest,
53
+ max_tokens: 64000,
54
+ temperature: 1,
55
+ top_p: 0,
56
+ top_k: 40,
57
+ stop_sequences: ['stop'],
58
+ tools: [{ name: 't', input_schema: {} }],
59
+ tool_choice: { type: 'tool', name: 't' },
60
+ thinking: { type: 'enabled', budget_tokens: 32000 },
61
+ output_config: { effort: 'high' },
62
+ };
63
+ expect(validateConfig(richModel, request)).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe('validateConfig — every accepted knob at its boundary stays clean', () => {
68
+ const boundaryCases: Array<{ label: string; patch: Record<string, unknown> }> = [
69
+ { label: 'max_tokens at ceiling', patch: { max_tokens: 64000 } },
70
+ { label: 'temperature at min', patch: { temperature: 0 } },
71
+ { label: 'temperature at max', patch: { temperature: 1 } },
72
+ { label: 'top_p at min', patch: { top_p: 0 } },
73
+ { label: 'top_p at max', patch: { top_p: 1 } },
74
+ { label: 'thinking budget at min', patch: { thinking: { type: 'enabled', budget_tokens: 1024 } } },
75
+ { label: 'thinking budget at max', patch: { thinking: { type: 'enabled', budget_tokens: 32000 } } },
76
+ { label: 'effort lowest allowed', patch: { output_config: { effort: 'low' } } },
77
+ { label: 'effort highest allowed', patch: { output_config: { effort: 'high' } } },
78
+ ];
79
+
80
+ for (const boundaryCase of boundaryCases) {
81
+ it(`${boundaryCase.label} produces no finding`, () => {
82
+ expect(validateConfig(richModel, { ...baseRequest, ...boundaryCase.patch })).toEqual([]);
83
+ });
84
+ }
85
+ });
86
+
87
+ describe('validateConfig — every out-of-bounds value warns with the right code', () => {
88
+ const violationCases: Array<{ label: string; patch: Record<string, unknown>; field: string; code: string }> = [
89
+ { label: 'max_tokens over ceiling', patch: { max_tokens: 200000 }, field: 'max_tokens', code: 'above_ceiling' },
90
+ { label: 'temperature over max', patch: { temperature: 2 }, field: 'temperature', code: 'above_maximum' },
91
+ { label: 'temperature under min', patch: { temperature: -1 }, field: 'temperature', code: 'below_minimum' },
92
+ { label: 'top_p over max', patch: { top_p: 5 }, field: 'top_p', code: 'above_maximum' },
93
+ { label: 'thinking budget over max', patch: { thinking: { type: 'enabled', budget_tokens: 99999 } }, field: 'thinking.budget_tokens', code: 'above_maximum' },
94
+ { label: 'thinking budget under min', patch: { thinking: { type: 'enabled', budget_tokens: 1 } }, field: 'thinking.budget_tokens', code: 'below_minimum' },
95
+ { label: 'thinking mode not allowed', patch: { thinking: { type: 'mystery' } }, field: 'thinking.type', code: 'not_in_allowed_set' },
96
+ { label: 'effort not in allowed set', patch: { output_config: { effort: 'max' } }, field: 'output_config.effort', code: 'not_in_allowed_set' },
97
+ { label: 'tool_choice not in allowed set', patch: { tool_choice: { type: 'mystery' } }, field: 'tool_choice.type', code: 'not_in_allowed_set' },
98
+ ];
99
+
100
+ for (const violationCase of violationCases) {
101
+ it(`${violationCase.label} warns ${violationCase.code}`, () => {
102
+ const findings = validateConfig(richModel, { ...baseRequest, ...violationCase.patch });
103
+ expect(codesFor(findings, violationCase.field)).toEqual([violationCase.code]);
104
+ expect(findings.every(finding => finding.severity === 'warning')).toBe(true);
105
+ });
106
+ }
107
+ });
108
+
109
+ describe('validateConfig — a knob the model rejects warns not_accepted', () => {
110
+ const rejectedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
111
+ { label: 'temperature', patch: { temperature: 0.5 }, field: 'temperature' },
112
+ { label: 'thinking', patch: { thinking: { type: 'enabled', budget_tokens: 2048 } }, field: 'thinking' },
113
+ { label: 'effort', patch: { output_config: { effort: 'high' } }, field: 'output_config.effort' },
114
+ { label: 'vision', patch: { messages: [{ role: 'user', content: [{ type: 'image', source: { type: 'url', url: 'x' } }] }] }, field: 'messages' },
115
+ ];
116
+
117
+ for (const rejectedCase of rejectedCases) {
118
+ it(`${rejectedCase.label} on a reasoning model warns not_accepted`, () => {
119
+ const findings = validateConfig(reasoningModel, { ...baseRequest, ...rejectedCase.patch });
120
+ expect(codesFor(findings, rejectedCase.field)).toEqual(['not_accepted']);
121
+ expect(findings[0].severity).toBe('warning');
122
+ });
123
+ }
124
+ });
125
+
126
+ describe('validateConfig — fail open when the profile is silent', () => {
127
+ it('never warns about a knob the profile does not describe', () => {
128
+ const request = {
129
+ ...baseRequest,
130
+ max_tokens: 9_000_000,
131
+ temperature: 42,
132
+ top_k: 1000,
133
+ thinking: { type: 'anything', budget_tokens: 5 },
134
+ output_config: { effort: 'nonexistent' },
135
+ };
136
+ expect(validateConfig(unknownModel, request)).toEqual([]);
137
+ });
138
+ });
139
+
140
+ describe('validateConfig — malformed native fields are format errors', () => {
141
+ const malformedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
142
+ { label: 'non-numeric temperature', patch: { temperature: 'hot' }, field: 'temperature' },
143
+ { label: 'non-object thinking', patch: { thinking: 'deep' }, field: 'thinking' },
144
+ { label: 'non-string thinking.type', patch: { thinking: { type: 123 } }, field: 'thinking.type' },
145
+ { label: 'non-numeric thinking.budget_tokens', patch: { thinking: { type: 'enabled', budget_tokens: 'lots' } }, field: 'thinking.budget_tokens' },
146
+ { label: 'non-object output_config', patch: { output_config: 'high' }, field: 'output_config' },
147
+ { label: 'non-string effort', patch: { output_config: { effort: 3 } }, field: 'output_config.effort' },
148
+ { label: 'non-object tool_choice', patch: { tool_choice: 'auto' }, field: 'tool_choice' },
149
+ { label: 'non-string tool_choice.type', patch: { tool_choice: { type: 7 } }, field: 'tool_choice.type' },
150
+ ];
151
+
152
+ for (const malformedCase of malformedCases) {
153
+ it(`flags ${malformedCase.label} as an error`, () => {
154
+ const findings = validateConfig(richModel, { ...baseRequest, ...malformedCase.patch });
155
+ expect(findings).toEqual([
156
+ expect.objectContaining({ severity: 'error', field: malformedCase.field, code: 'wrong_type' }),
157
+ ]);
158
+ });
159
+
160
+ it(`flags ${malformedCase.label} as an error even when the profile is silent`, () => {
161
+ const findings = validateConfig(unknownModel, { ...baseRequest, ...malformedCase.patch });
162
+ expect(findings).toEqual([
163
+ expect.objectContaining({ severity: 'error', field: malformedCase.field, code: 'wrong_type' }),
164
+ ]);
165
+ });
166
+ }
167
+
168
+ it('flags a non-object request as an error', () => {
169
+ const findings = validateConfig(richModel, 'not an object');
170
+ expect(findings).toEqual([
171
+ { severity: 'error', field: 'request', code: 'wrong_type', detail: 'request must be an object' },
172
+ ]);
173
+ });
174
+ });
@@ -0,0 +1,438 @@
1
+ import type { AnthropicEffort, AnthropicThinking, AnthropicToolChoice } from './anthropic.js';
2
+ import type {
3
+ AnthropicProfile,
4
+ AnthropicThinkingProfile,
5
+ GoogleProfile,
6
+ ModelProfile,
7
+ NumericRange,
8
+ OpenAIGenericProfile,
9
+ OpenAIProProfile,
10
+ OpenAIToolChoiceOption,
11
+ ProfileFinding,
12
+ } from './profile.js';
13
+
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+
18
+ function readNumber(findings: ProfileFinding[], request: Record<string, unknown>, field: string, label: string = field): number | undefined {
19
+ const raw = request[field];
20
+ if (raw === undefined) return undefined;
21
+ if (typeof raw !== 'number') {
22
+ findings.push({ severity: 'error', field: label, code: 'wrong_type', detail: `${label} must be a number` });
23
+ return undefined;
24
+ }
25
+ return raw;
26
+ }
27
+
28
+ function checkRange(
29
+ findings: ProfileFinding[],
30
+ request: Record<string, unknown>,
31
+ field: string,
32
+ rule: NumericRange | false | undefined,
33
+ label: string = field,
34
+ ): void {
35
+ const value = readNumber(findings, request, field, label);
36
+ if (value === undefined || rule === undefined) return;
37
+ if (rule === false) {
38
+ findings.push({ severity: 'warning', field: label, code: 'not_accepted', detail: `${label} is not accepted by this model` });
39
+ return;
40
+ }
41
+ if (rule.min !== undefined && value < rule.min) {
42
+ findings.push({ severity: 'warning', field: label, code: 'below_minimum', detail: `${label} ${value} is below the accepted minimum ${rule.min}` });
43
+ }
44
+ if (rule.max !== undefined && value > rule.max) {
45
+ findings.push({ severity: 'warning', field: label, code: 'above_maximum', detail: `${label} ${value} is above the accepted maximum ${rule.max}` });
46
+ }
47
+ }
48
+
49
+ function checkCeiling(
50
+ findings: ProfileFinding[],
51
+ request: Record<string, unknown>,
52
+ field: string,
53
+ ceiling: number | undefined,
54
+ label: string = field,
55
+ ): void {
56
+ const value = readNumber(findings, request, field, label);
57
+ if (value === undefined || ceiling === undefined) return;
58
+ if (value > ceiling) {
59
+ findings.push({ severity: 'warning', field: label, code: 'above_ceiling', detail: `${label} ${value} exceeds this model's ceiling ${ceiling}` });
60
+ }
61
+ }
62
+
63
+ function checkAccepted(
64
+ findings: ProfileFinding[],
65
+ request: Record<string, unknown>,
66
+ field: string,
67
+ accepted: boolean | undefined,
68
+ label: string = field,
69
+ ): void {
70
+ if (accepted !== false) return;
71
+ if (request[field] === undefined) return;
72
+ findings.push({ severity: 'warning', field: label, code: 'not_accepted', detail: `${label} is not accepted by this model` });
73
+ }
74
+
75
+ function checkEffort(
76
+ findings: ProfileFinding[],
77
+ request: Record<string, unknown>,
78
+ allowed: AnthropicEffort[] | false | undefined,
79
+ ): void {
80
+ const outputConfig = request.output_config;
81
+ if (outputConfig === undefined) return;
82
+ if (!isRecord(outputConfig)) {
83
+ findings.push({ severity: 'error', field: 'output_config', code: 'wrong_type', detail: 'output_config must be an object' });
84
+ return;
85
+ }
86
+ const effort = outputConfig.effort;
87
+ if (effort === undefined) return;
88
+ if (typeof effort !== 'string') {
89
+ findings.push({ severity: 'error', field: 'output_config.effort', code: 'wrong_type', detail: 'effort must be a string' });
90
+ return;
91
+ }
92
+ if (allowed === undefined) return;
93
+ if (allowed === false) {
94
+ findings.push({ severity: 'warning', field: 'output_config.effort', code: 'not_accepted', detail: 'effort is not accepted by this model' });
95
+ return;
96
+ }
97
+ if (!allowed.includes(effort as AnthropicEffort)) {
98
+ findings.push({ severity: 'warning', field: 'output_config.effort', code: 'not_in_allowed_set', detail: `effort '${effort}' is not one of ${allowed.join(', ')}` });
99
+ }
100
+ }
101
+
102
+ function checkThinking(
103
+ findings: ProfileFinding[],
104
+ request: Record<string, unknown>,
105
+ rule: AnthropicThinkingProfile | false | undefined,
106
+ ): void {
107
+ const thinking = request.thinking;
108
+ if (thinking === undefined) return;
109
+ if (!isRecord(thinking)) {
110
+ findings.push({ severity: 'error', field: 'thinking', code: 'wrong_type', detail: 'thinking must be an object' });
111
+ return;
112
+ }
113
+ const mode = thinking.type;
114
+ if (typeof mode !== 'string') {
115
+ findings.push({ severity: 'error', field: 'thinking.type', code: 'wrong_type', detail: 'thinking.type must be a string' });
116
+ return;
117
+ }
118
+ const budget = thinking.budget_tokens;
119
+ if (budget !== undefined && typeof budget !== 'number') {
120
+ findings.push({ severity: 'error', field: 'thinking.budget_tokens', code: 'wrong_type', detail: 'thinking.budget_tokens must be a number' });
121
+ return;
122
+ }
123
+ if (rule === undefined) return;
124
+ if (rule === false) {
125
+ findings.push({ severity: 'warning', field: 'thinking', code: 'not_accepted', detail: 'thinking is not accepted by this model' });
126
+ return;
127
+ }
128
+ if (!rule.modes.includes(mode as AnthropicThinking['type'])) {
129
+ findings.push({ severity: 'warning', field: 'thinking.type', code: 'not_in_allowed_set', detail: `thinking mode '${mode}' is not one of ${rule.modes.join(', ')}` });
130
+ }
131
+ if (rule.budgetTokens !== undefined && typeof budget === 'number') {
132
+ if (rule.budgetTokens.min !== undefined && budget < rule.budgetTokens.min) {
133
+ findings.push({ severity: 'warning', field: 'thinking.budget_tokens', code: 'below_minimum', detail: `budget_tokens ${budget} is below the accepted minimum ${rule.budgetTokens.min}` });
134
+ }
135
+ if (rule.budgetTokens.max !== undefined && budget > rule.budgetTokens.max) {
136
+ findings.push({ severity: 'warning', field: 'thinking.budget_tokens', code: 'above_maximum', detail: `budget_tokens ${budget} is above the accepted maximum ${rule.budgetTokens.max}` });
137
+ }
138
+ }
139
+ }
140
+
141
+ function checkToolChoice(
142
+ findings: ProfileFinding[],
143
+ request: Record<string, unknown>,
144
+ allowed: AnthropicToolChoice['type'][] | undefined,
145
+ ): void {
146
+ const toolChoice = request.tool_choice;
147
+ if (toolChoice === undefined) return;
148
+ if (!isRecord(toolChoice)) {
149
+ findings.push({ severity: 'error', field: 'tool_choice', code: 'wrong_type', detail: 'tool_choice must be an object' });
150
+ return;
151
+ }
152
+ const choiceType = toolChoice.type;
153
+ if (typeof choiceType !== 'string') {
154
+ findings.push({ severity: 'error', field: 'tool_choice.type', code: 'wrong_type', detail: 'tool_choice.type must be a string' });
155
+ return;
156
+ }
157
+ if (allowed === undefined) return;
158
+ if (!allowed.includes(choiceType as AnthropicToolChoice['type'])) {
159
+ findings.push({ severity: 'warning', field: 'tool_choice.type', code: 'not_in_allowed_set', detail: `tool_choice '${choiceType}' is not one of ${allowed.join(', ')}` });
160
+ }
161
+ }
162
+
163
+ function checkVision(
164
+ findings: ProfileFinding[],
165
+ request: Record<string, unknown>,
166
+ vision: boolean | undefined,
167
+ ): void {
168
+ if (vision !== false) return;
169
+ const messages = request.messages;
170
+ if (!Array.isArray(messages)) return;
171
+ const hasImage = messages.some(message =>
172
+ isRecord(message) &&
173
+ Array.isArray(message.content) &&
174
+ message.content.some(block => isRecord(block) && block.type === 'image'),
175
+ );
176
+ if (hasImage) {
177
+ findings.push({ severity: 'warning', field: 'messages', code: 'not_accepted', detail: 'this model does not accept image content blocks' });
178
+ }
179
+ }
180
+
181
+ function checkStringEnum(
182
+ findings: ProfileFinding[],
183
+ field: string,
184
+ raw: unknown,
185
+ allowed: readonly string[] | false | undefined,
186
+ ): void {
187
+ if (raw === undefined) return;
188
+ if (typeof raw !== 'string') {
189
+ findings.push({ severity: 'error', field, code: 'wrong_type', detail: `${field} must be a string` });
190
+ return;
191
+ }
192
+ if (allowed === undefined) return;
193
+ if (allowed === false) {
194
+ findings.push({ severity: 'warning', field, code: 'not_accepted', detail: `${field} is not accepted by this model` });
195
+ return;
196
+ }
197
+ if (!allowed.includes(raw)) {
198
+ findings.push({ severity: 'warning', field, code: 'not_in_allowed_set', detail: `${field} '${raw}' is not one of ${allowed.join(', ')}` });
199
+ }
200
+ }
201
+
202
+ function checkBooleanField(
203
+ findings: ProfileFinding[],
204
+ request: Record<string, unknown>,
205
+ field: string,
206
+ accepted: boolean | undefined,
207
+ ): void {
208
+ const raw = request[field];
209
+ if (raw === undefined) return;
210
+ if (typeof raw !== 'boolean') {
211
+ findings.push({ severity: 'error', field, code: 'wrong_type', detail: `${field} must be a boolean` });
212
+ return;
213
+ }
214
+ if (accepted === false) {
215
+ findings.push({ severity: 'warning', field, code: 'not_accepted', detail: `${field} is not accepted by this model` });
216
+ }
217
+ }
218
+
219
+ function checkObjectField(
220
+ findings: ProfileFinding[],
221
+ request: Record<string, unknown>,
222
+ field: string,
223
+ accepted: boolean | undefined,
224
+ ): void {
225
+ const raw = request[field];
226
+ if (raw === undefined) return;
227
+ if (!isRecord(raw)) {
228
+ findings.push({ severity: 'error', field, code: 'wrong_type', detail: `${field} must be an object` });
229
+ return;
230
+ }
231
+ if (accepted === false) {
232
+ findings.push({ severity: 'warning', field, code: 'not_accepted', detail: `${field} is not accepted by this model` });
233
+ }
234
+ }
235
+
236
+ function checkOpenAIToolChoice(
237
+ findings: ProfileFinding[],
238
+ request: Record<string, unknown>,
239
+ allowed: OpenAIToolChoiceOption[] | undefined,
240
+ ): void {
241
+ const toolChoice = request.tool_choice;
242
+ if (toolChoice === undefined) return;
243
+ if (typeof toolChoice !== 'string' && !isRecord(toolChoice)) {
244
+ findings.push({ severity: 'error', field: 'tool_choice', code: 'wrong_type', detail: 'tool_choice must be a string or an object' });
245
+ return;
246
+ }
247
+ let choice: string;
248
+ if (typeof toolChoice === 'string') {
249
+ choice = toolChoice;
250
+ } else {
251
+ const choiceType = toolChoice.type;
252
+ if (typeof choiceType !== 'string') {
253
+ findings.push({ severity: 'error', field: 'tool_choice.type', code: 'wrong_type', detail: 'tool_choice.type must be a string' });
254
+ return;
255
+ }
256
+ choice = choiceType;
257
+ }
258
+ if (allowed === undefined) return;
259
+ if (!allowed.includes(choice as OpenAIToolChoiceOption)) {
260
+ findings.push({ severity: 'warning', field: 'tool_choice', code: 'not_in_allowed_set', detail: `tool_choice '${choice}' is not one of ${allowed.join(', ')}` });
261
+ }
262
+ }
263
+
264
+ function contentHasPartType(content: unknown, partType: string): boolean {
265
+ return Array.isArray(content) && content.some(part => isRecord(part) && part.type === partType);
266
+ }
267
+
268
+ function checkOpenAIChatVision(
269
+ findings: ProfileFinding[],
270
+ request: Record<string, unknown>,
271
+ vision: boolean | undefined,
272
+ ): void {
273
+ if (vision !== false) return;
274
+ const messages = request.messages;
275
+ if (!Array.isArray(messages)) return;
276
+ const hasImage = messages.some(message => isRecord(message) && contentHasPartType(message.content, 'image_url'));
277
+ if (hasImage) {
278
+ findings.push({ severity: 'warning', field: 'messages', code: 'not_accepted', detail: 'this model does not accept image content parts' });
279
+ }
280
+ }
281
+
282
+ function checkOpenAIResponsesVision(
283
+ findings: ProfileFinding[],
284
+ request: Record<string, unknown>,
285
+ vision: boolean | undefined,
286
+ ): void {
287
+ if (vision !== false) return;
288
+ const input = request.input;
289
+ if (!Array.isArray(input)) return;
290
+ const hasImage = input.some(item =>
291
+ isRecord(item) && item.type === 'message' && contentHasPartType(item.content, 'input_image'),
292
+ );
293
+ if (hasImage) {
294
+ findings.push({ severity: 'warning', field: 'input', code: 'not_accepted', detail: 'this model does not accept image content parts' });
295
+ }
296
+ }
297
+
298
+ function readNestedObject(
299
+ findings: ProfileFinding[],
300
+ request: Record<string, unknown>,
301
+ field: string,
302
+ label: string = field,
303
+ ): Record<string, unknown> | undefined {
304
+ const raw = request[field];
305
+ if (raw === undefined) return undefined;
306
+ if (!isRecord(raw)) {
307
+ findings.push({ severity: 'error', field: label, code: 'wrong_type', detail: `${label} must be an object` });
308
+ return undefined;
309
+ }
310
+ return raw;
311
+ }
312
+
313
+ function validateAnthropic(profile: AnthropicProfile, request: Record<string, unknown>): ProfileFinding[] {
314
+ const findings: ProfileFinding[] = [];
315
+ checkCeiling(findings, request, 'max_tokens', profile.maxTokensCeiling);
316
+ checkRange(findings, request, 'temperature', profile.temperature);
317
+ checkRange(findings, request, 'top_p', profile.topP);
318
+ checkAccepted(findings, request, 'top_k', profile.topK);
319
+ checkAccepted(findings, request, 'stop_sequences', profile.stopSequences);
320
+ checkAccepted(findings, request, 'tools', profile.tools);
321
+ checkToolChoice(findings, request, profile.toolChoice);
322
+ checkThinking(findings, request, profile.thinking);
323
+ checkEffort(findings, request, profile.effort);
324
+ checkVision(findings, request, profile.vision);
325
+ return findings;
326
+ }
327
+
328
+ function validateOpenAIGeneric(profile: OpenAIGenericProfile, request: Record<string, unknown>): ProfileFinding[] {
329
+ const findings: ProfileFinding[] = [];
330
+ checkCeiling(findings, request, 'max_completion_tokens', profile.maxTokensCeiling);
331
+ checkCeiling(findings, request, 'max_tokens', profile.maxTokensCeiling);
332
+ checkRange(findings, request, 'temperature', profile.temperature);
333
+ checkRange(findings, request, 'top_p', profile.topP);
334
+ checkRange(findings, request, 'frequency_penalty', profile.frequencyPenalty);
335
+ checkRange(findings, request, 'presence_penalty', profile.presencePenalty);
336
+ checkAccepted(findings, request, 'tools', profile.tools);
337
+ checkOpenAIToolChoice(findings, request, profile.toolChoice);
338
+ checkBooleanField(findings, request, 'parallel_tool_calls', profile.parallelToolCalls);
339
+ checkObjectField(findings, request, 'response_format', profile.responseFormat);
340
+ checkStringEnum(findings, 'reasoning_effort', request.reasoning_effort, profile.reasoningEffort);
341
+ checkStringEnum(findings, 'verbosity', request.verbosity, profile.verbosity);
342
+ checkOpenAIChatVision(findings, request, profile.vision);
343
+ return findings;
344
+ }
345
+
346
+ function validateOpenAIPro(profile: OpenAIProProfile, request: Record<string, unknown>): ProfileFinding[] {
347
+ const findings: ProfileFinding[] = [];
348
+ checkCeiling(findings, request, 'max_output_tokens', profile.maxTokensCeiling);
349
+ checkRange(findings, request, 'temperature', profile.temperature);
350
+ checkRange(findings, request, 'top_p', profile.topP);
351
+ checkAccepted(findings, request, 'tools', profile.tools);
352
+ checkOpenAIToolChoice(findings, request, profile.toolChoice);
353
+ checkBooleanField(findings, request, 'parallel_tool_calls', profile.parallelToolCalls);
354
+ const reasoning = readNestedObject(findings, request, 'reasoning');
355
+ if (reasoning !== undefined) {
356
+ checkStringEnum(findings, 'reasoning.effort', reasoning.effort, profile.reasoningEffort);
357
+ }
358
+ const text = readNestedObject(findings, request, 'text');
359
+ if (text !== undefined) {
360
+ checkStringEnum(findings, 'text.verbosity', text.verbosity, profile.verbosity);
361
+ }
362
+ checkOpenAIResponsesVision(findings, request, profile.vision);
363
+ return findings;
364
+ }
365
+
366
+ function partsContainMedia(parts: unknown): boolean {
367
+ return Array.isArray(parts) && parts.some(part =>
368
+ isRecord(part) && (part.inlineData !== undefined || part.fileData !== undefined),
369
+ );
370
+ }
371
+
372
+ function checkGoogleVision(
373
+ findings: ProfileFinding[],
374
+ request: Record<string, unknown>,
375
+ vision: boolean | undefined,
376
+ ): void {
377
+ if (vision !== false) return;
378
+ const contents = request.contents;
379
+ if (!Array.isArray(contents)) return;
380
+ const hasMedia = contents.some(content => isRecord(content) && partsContainMedia(content.parts));
381
+ if (hasMedia) {
382
+ findings.push({ severity: 'warning', field: 'contents', code: 'not_accepted', detail: 'this model does not accept media parts' });
383
+ }
384
+ }
385
+
386
+ function validateGoogle(profile: GoogleProfile, request: Record<string, unknown>): ProfileFinding[] {
387
+ const findings: ProfileFinding[] = [];
388
+ const generationConfig = readNestedObject(findings, request, 'generationConfig');
389
+ if (generationConfig !== undefined) {
390
+ checkCeiling(findings, generationConfig, 'maxOutputTokens', profile.maxTokensCeiling, 'generationConfig.maxOutputTokens');
391
+ checkRange(findings, generationConfig, 'temperature', profile.temperature, 'generationConfig.temperature');
392
+ checkRange(findings, generationConfig, 'topP', profile.topP, 'generationConfig.topP');
393
+ checkAccepted(findings, generationConfig, 'topK', profile.topK, 'generationConfig.topK');
394
+ checkAccepted(findings, generationConfig, 'stopSequences', profile.stopSequences, 'generationConfig.stopSequences');
395
+ if (generationConfig.responseSchema !== undefined && profile.responseSchema === false) {
396
+ findings.push({ severity: 'warning', field: 'generationConfig.responseSchema', code: 'not_accepted', detail: 'responseSchema is not accepted by this model' });
397
+ }
398
+ const thinkingConfig = generationConfig.thinkingConfig;
399
+ if (thinkingConfig !== undefined) {
400
+ if (!isRecord(thinkingConfig)) {
401
+ findings.push({ severity: 'error', field: 'generationConfig.thinkingConfig', code: 'wrong_type', detail: 'generationConfig.thinkingConfig must be an object' });
402
+ } else {
403
+ checkRange(findings, thinkingConfig, 'thinkingBudget', profile.thinkingBudget, 'generationConfig.thinkingConfig.thinkingBudget');
404
+ }
405
+ }
406
+ }
407
+ checkAccepted(findings, request, 'tools', profile.tools);
408
+ const toolConfig = readNestedObject(findings, request, 'toolConfig');
409
+ if (toolConfig !== undefined) {
410
+ const functionCallingConfig = readNestedObject(findings, toolConfig, 'functionCallingConfig', 'toolConfig.functionCallingConfig');
411
+ if (functionCallingConfig !== undefined) {
412
+ checkStringEnum(findings, 'toolConfig.functionCallingConfig.mode', functionCallingConfig.mode, profile.functionCallingModes);
413
+ }
414
+ }
415
+ checkGoogleVision(findings, request, profile.vision);
416
+ return findings;
417
+ }
418
+
419
+ export function validateConfig(profile: ModelProfile, request: unknown): ProfileFinding[] {
420
+ if (!isRecord(request)) {
421
+ return [{ severity: 'error', field: 'request', code: 'wrong_type', detail: 'request must be an object' }];
422
+ }
423
+ switch (profile.kind) {
424
+ case 'anthropic':
425
+ return validateAnthropic(profile, request);
426
+ case 'openai_generic':
427
+ return validateOpenAIGeneric(profile, request);
428
+ case 'openai_pro':
429
+ return validateOpenAIPro(profile, request);
430
+ case 'google':
431
+ return validateGoogle(profile, request);
432
+ default: {
433
+ const exhaustive: never = profile;
434
+ void exhaustive;
435
+ return [];
436
+ }
437
+ }
438
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "switchboard-local",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Local models in the pi model picker, managed end to end",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/Valni-Labs/switchboard-local"
12
+ },
13
+ "license": "MIT",
14
+ "engines": {
15
+ "node": ">=22.19.0"
16
+ },
17
+ "scripts": {
18
+ "test": "npm run typecheck && node --test \"extensions/local/tests/*.test.ts\"",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "pi": {
22
+ "extensions": [
23
+ "./extensions"
24
+ ]
25
+ },
26
+ "dependencies": {
27
+ "@valni/switchboard": "git+https://github.com/Valni-Labs/switchboard-ts.git#d132540a1fd86491ffabb9327ec38bb903f709a3"
28
+ },
29
+ "devDependencies": {
30
+ "@earendil-works/pi-ai": "0.82.1",
31
+ "@earendil-works/pi-coding-agent": "0.82.1",
32
+ "typescript": "5.9.3"
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2023",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "allowImportingTsExtensions": true,
7
+ "noEmit": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["extensions/**/*.ts"]
13
+ }