@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.
- package/LICENSE +52 -0
- package/package.json +18 -5
- package/runtime/LICENSE +52 -0
- package/runtime/assets/clankolas.png +0 -0
- package/runtime/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/runtime/examples/extensions/bash-spawn-hook.ts +30 -0
- package/runtime/examples/extensions/bookmark.ts +50 -0
- package/runtime/examples/extensions/border-status-editor.ts +145 -0
- package/runtime/examples/extensions/built-in-tool-renderer.ts +249 -0
- package/runtime/examples/extensions/claude-rules.ts +86 -0
- package/runtime/examples/extensions/commands.ts +72 -0
- package/runtime/examples/extensions/confirm-destructive.ts +59 -0
- package/runtime/examples/extensions/custom-footer.ts +64 -0
- package/runtime/examples/extensions/custom-header.ts +73 -0
- package/runtime/examples/extensions/custom-provider-anthropic/index.ts +604 -0
- package/runtime/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/runtime/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/runtime/examples/extensions/dirty-repo-guard.ts +56 -0
- package/runtime/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
- package/runtime/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/runtime/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/runtime/examples/extensions/doom-overlay/doom/doomgeneric_valni.c +72 -0
- package/runtime/examples/extensions/doom-overlay/doom-component.ts +132 -0
- package/runtime/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/runtime/examples/extensions/doom-overlay/doom-keys.ts +104 -0
- package/runtime/examples/extensions/doom-overlay/index.ts +74 -0
- package/runtime/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/runtime/examples/extensions/dynamic-resources/dynamic.json +78 -0
- package/runtime/examples/extensions/dynamic-resources/index.ts +13 -0
- package/runtime/examples/extensions/dynamic-tools.ts +74 -0
- package/runtime/examples/extensions/entry-renderer.ts +41 -0
- package/runtime/examples/extensions/event-bus.ts +43 -0
- package/runtime/examples/extensions/file-trigger.ts +41 -0
- package/runtime/examples/extensions/git-checkpoint.ts +53 -0
- package/runtime/examples/extensions/git-merge-and-resolve.ts +115 -0
- package/runtime/examples/extensions/github-issue-autocomplete.ts +185 -0
- package/runtime/examples/extensions/hello.ts +26 -0
- package/runtime/examples/extensions/hidden-thinking-label.ts +53 -0
- package/runtime/examples/extensions/inline-bash.ts +94 -0
- package/runtime/examples/extensions/input-transform-streaming.ts +39 -0
- package/runtime/examples/extensions/input-transform.ts +43 -0
- package/runtime/examples/extensions/interactive-shell.ts +196 -0
- package/runtime/examples/extensions/mac-system-theme.ts +47 -0
- package/runtime/examples/extensions/message-renderer.ts +59 -0
- package/runtime/examples/extensions/minimal-mode.ts +426 -0
- package/runtime/examples/extensions/modal-editor.ts +85 -0
- package/runtime/examples/extensions/model-status.ts +31 -0
- package/runtime/examples/extensions/notify.ts +55 -0
- package/runtime/examples/extensions/overlay-qa-tests.ts +1450 -0
- package/runtime/examples/extensions/overlay-test.ts +153 -0
- package/runtime/examples/extensions/permission-gate.ts +34 -0
- package/runtime/examples/extensions/pirate.ts +47 -0
- package/runtime/examples/extensions/plan-mode/index.ts +390 -0
- package/runtime/examples/extensions/plan-mode/utils.ts +168 -0
- package/runtime/examples/extensions/preset.ts +436 -0
- package/runtime/examples/extensions/project-trust.ts +64 -0
- package/runtime/examples/extensions/prompt-customizer.ts +97 -0
- package/runtime/examples/extensions/protected-paths.ts +30 -0
- package/runtime/examples/extensions/provider-payload.ts +18 -0
- package/runtime/examples/extensions/question.ts +278 -0
- package/runtime/examples/extensions/questionnaire.ts +440 -0
- package/runtime/examples/extensions/rainbow-editor.ts +88 -0
- package/runtime/examples/extensions/reload-runtime.ts +37 -0
- package/runtime/examples/extensions/rpc-demo.ts +118 -0
- package/runtime/examples/extensions/sandbox/index.ts +321 -0
- package/runtime/examples/extensions/sandbox/package-lock.json +92 -0
- package/runtime/examples/extensions/sandbox/package.json +19 -0
- package/runtime/examples/extensions/send-user-message.ts +97 -0
- package/runtime/examples/extensions/session-name.ts +27 -0
- package/runtime/examples/extensions/shutdown-command.ts +63 -0
- package/runtime/examples/extensions/snake.ts +343 -0
- package/runtime/examples/extensions/space-invaders.ts +560 -0
- package/runtime/examples/extensions/ssh.ts +220 -0
- package/runtime/examples/extensions/status-line.ts +32 -0
- package/runtime/examples/extensions/structured-output.ts +65 -0
- package/runtime/examples/extensions/subagent/agents.ts +126 -0
- package/runtime/examples/extensions/subagent/index.ts +1009 -0
- package/runtime/examples/extensions/system-prompt-header.ts +17 -0
- package/runtime/examples/extensions/tic-tac-toe.ts +1008 -0
- package/runtime/examples/extensions/timed-confirm.ts +70 -0
- package/runtime/examples/extensions/titlebar-spinner.ts +58 -0
- package/runtime/examples/extensions/todo.ts +297 -0
- package/runtime/examples/extensions/tool-override.ts +144 -0
- package/runtime/examples/extensions/tools.ts +146 -0
- package/runtime/examples/extensions/trigger-compact.ts +50 -0
- package/runtime/examples/extensions/truncated-tool.ts +195 -0
- package/runtime/examples/extensions/widget-placement.ts +9 -0
- package/runtime/examples/extensions/with-deps/index.ts +32 -0
- package/runtime/examples/extensions/with-deps/package-lock.json +31 -0
- package/runtime/examples/extensions/with-deps/package.json +22 -0
- package/runtime/examples/extensions/working-indicator.ts +123 -0
- package/runtime/examples/extensions/working-message-test.ts +25 -0
- package/runtime/examples/rpc-extension-ui.ts +632 -0
- package/runtime/examples/sdk/01-minimal.ts +26 -0
- package/runtime/examples/sdk/02-custom-model.ts +43 -0
- package/runtime/examples/sdk/03-custom-prompt.ts +70 -0
- package/runtime/examples/sdk/04-skills.ts +55 -0
- package/runtime/examples/sdk/05-tools.ts +48 -0
- package/runtime/examples/sdk/06-extensions.ts +94 -0
- package/runtime/examples/sdk/07-context-files.ts +42 -0
- package/runtime/examples/sdk/08-prompt-templates.ts +51 -0
- package/runtime/examples/sdk/09-api-keys-and-oauth.ts +34 -0
- package/runtime/examples/sdk/10-settings.ts +53 -0
- package/runtime/examples/sdk/11-sessions.ts +52 -0
- package/runtime/examples/sdk/12-full-control.ts +71 -0
- package/runtime/examples/sdk/13-session-runtime.ts +67 -0
- package/runtime/export-html/ansi-to-html.js +249 -0
- package/runtime/export-html/index.js +226 -0
- package/runtime/export-html/template.css +1066 -0
- package/runtime/export-html/template.html +55 -0
- package/runtime/export-html/template.js +1864 -0
- package/runtime/export-html/tool-renderer.js +108 -0
- package/runtime/export-html/vendor/highlight.min.js +1213 -0
- package/runtime/export-html/vendor/marked.min.js +78 -0
- package/runtime/extensions/pi-switchboard/.github/workflows/ci.yml +51 -0
- package/runtime/extensions/pi-switchboard/LICENSE +22 -0
- package/runtime/extensions/pi-switchboard/README.md +85 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +183 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/config.ts +97 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/connections.ts +386 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/constants.ts +12 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/device-auth.ts +127 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/envelope.ts +199 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/errors.ts +180 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +125 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/participant.ts +17 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/sessionStream.ts +131 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +136 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +16 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +37 -0
- package/runtime/extensions/pi-switchboard/package.json +30 -0
- package/runtime/extensions/pi-switchboard/tsconfig.json +13 -0
- package/runtime/extensions/switchboard-local/.github/workflows/ci.yml +80 -0
- package/runtime/extensions/switchboard-local/.github/workflows/release.yml +46 -0
- package/runtime/extensions/switchboard-local/README.md +98 -0
- package/runtime/extensions/switchboard-local/extensions/local/engine.ts +28 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/gguf.ts +324 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/llamaBuild.ts +62 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/mlx.ts +245 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/select.ts +38 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/support.ts +151 -0
- package/runtime/extensions/switchboard-local/extensions/local/hf.ts +106 -0
- package/runtime/extensions/switchboard-local/extensions/local/index.ts +67 -0
- package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +602 -0
- package/runtime/extensions/switchboard-local/extensions/local/paths.ts +44 -0
- package/runtime/extensions/switchboard-local/extensions/local/provider.ts +57 -0
- package/runtime/extensions/switchboard-local/extensions/local/store.ts +82 -0
- package/runtime/extensions/switchboard-local/extensions/local/ui.ts +60 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/apiTypes.js +74 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/client.js +292 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/deviceAuth.js +155 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/errors.js +95 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/index.js +8 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/messages.js +253 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/models.js +19 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/native.js +886 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/wire.js +85 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/package.json +39 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/package.json +29 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/anthropic.ts +127 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/google.ts +111 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/index.ts +8 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.test.ts +25 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.ts +57 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiGeneric.ts +103 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiPro.ts +89 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/profile.ts +120 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.test.ts +82 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.ts +74 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.google.test.ts +137 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.openai.test.ts +241 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.test.ts +174 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.ts +438 -0
- package/runtime/extensions/switchboard-local/package.json +34 -0
- package/runtime/extensions/switchboard-local/tsconfig.json +13 -0
- package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/runtime/package.json +98 -0
- package/runtime/photon_rs_bg.wasm +0 -0
- package/runtime/theme/dark.json +86 -0
- package/runtime/theme/light.json +85 -0
- package/runtime/theme/theme-schema.json +340 -0
- package/runtime/valni +4 -0
- package/README.md +0 -3
package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/profile.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { AnthropicEffort, AnthropicThinking, AnthropicToolChoice } from './anthropic.js';
|
|
2
|
+
import type { GoogleFunctionCallingMode } from './google.js';
|
|
3
|
+
import type { OpenAIChatRequest, OpenAIToolChoice } from './openaiGeneric.js';
|
|
4
|
+
import type { OpenAIResponsesRequest, OpenAIResponsesToolChoice } from './openaiPro.js';
|
|
5
|
+
|
|
6
|
+
export interface NumericRange {
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AnthropicThinkingMode = 'adaptive' | 'enabled' | 'disabled';
|
|
12
|
+
export type AnthropicToolChoiceOption = 'auto' | 'any' | 'none' | 'tool';
|
|
13
|
+
|
|
14
|
+
export interface AnthropicThinkingProfile {
|
|
15
|
+
modes: AnthropicThinkingMode[];
|
|
16
|
+
budgetTokens?: NumericRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnthropicProfile {
|
|
20
|
+
kind: 'anthropic';
|
|
21
|
+
model: string;
|
|
22
|
+
maxTokensCeiling?: number;
|
|
23
|
+
temperature?: NumericRange | false;
|
|
24
|
+
topP?: NumericRange | false;
|
|
25
|
+
topK?: boolean;
|
|
26
|
+
stopSequences?: boolean;
|
|
27
|
+
tools?: boolean;
|
|
28
|
+
toolChoice?: AnthropicToolChoiceOption[];
|
|
29
|
+
thinking?: AnthropicThinkingProfile | false;
|
|
30
|
+
effort?: AnthropicEffort[] | false;
|
|
31
|
+
vision?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type OpenAIToolChoiceOption = 'auto' | 'none' | 'required' | 'function';
|
|
35
|
+
|
|
36
|
+
export type OpenAIChatEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
37
|
+
export type OpenAIChatVerbosity = 'low' | 'medium' | 'high';
|
|
38
|
+
export type OpenAIResponsesEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
39
|
+
export type OpenAIResponsesVerbosity = 'low' | 'medium' | 'high';
|
|
40
|
+
|
|
41
|
+
type MutuallyAssignable<Left, Right> = [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false;
|
|
42
|
+
type AssertTrue<Value extends true> = Value;
|
|
43
|
+
|
|
44
|
+
export type ProfileEnumWireAssertions = [
|
|
45
|
+
AssertTrue<MutuallyAssignable<AnthropicThinkingMode, AnthropicThinking['type']>>,
|
|
46
|
+
AssertTrue<MutuallyAssignable<AnthropicToolChoiceOption, AnthropicToolChoice['type']>>,
|
|
47
|
+
AssertTrue<MutuallyAssignable<
|
|
48
|
+
OpenAIToolChoiceOption,
|
|
49
|
+
Extract<OpenAIToolChoice | OpenAIResponsesToolChoice, string> | Extract<OpenAIToolChoice | OpenAIResponsesToolChoice, { type: string }>['type']
|
|
50
|
+
>>,
|
|
51
|
+
AssertTrue<MutuallyAssignable<OpenAIChatEffort, NonNullable<OpenAIChatRequest['reasoning_effort']>>>,
|
|
52
|
+
AssertTrue<MutuallyAssignable<OpenAIChatVerbosity, NonNullable<OpenAIChatRequest['verbosity']>>>,
|
|
53
|
+
AssertTrue<MutuallyAssignable<OpenAIResponsesEffort, NonNullable<NonNullable<OpenAIResponsesRequest['reasoning']>['effort']>>>,
|
|
54
|
+
AssertTrue<MutuallyAssignable<OpenAIResponsesVerbosity, NonNullable<NonNullable<OpenAIResponsesRequest['text']>['verbosity']>>>,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export interface OpenAIGenericProfile {
|
|
58
|
+
kind: 'openai_generic';
|
|
59
|
+
model: string;
|
|
60
|
+
maxTokensCeiling?: number;
|
|
61
|
+
temperature?: NumericRange | false;
|
|
62
|
+
topP?: NumericRange | false;
|
|
63
|
+
frequencyPenalty?: NumericRange | false;
|
|
64
|
+
presencePenalty?: NumericRange | false;
|
|
65
|
+
tools?: boolean;
|
|
66
|
+
toolChoice?: OpenAIToolChoiceOption[];
|
|
67
|
+
parallelToolCalls?: boolean;
|
|
68
|
+
responseFormat?: boolean;
|
|
69
|
+
reasoningEffort?: OpenAIChatEffort[] | false;
|
|
70
|
+
verbosity?: OpenAIChatVerbosity[] | false;
|
|
71
|
+
vision?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface OpenAIProProfile {
|
|
75
|
+
kind: 'openai_pro';
|
|
76
|
+
model: string;
|
|
77
|
+
maxTokensCeiling?: number;
|
|
78
|
+
temperature?: NumericRange | false;
|
|
79
|
+
topP?: NumericRange | false;
|
|
80
|
+
tools?: boolean;
|
|
81
|
+
toolChoice?: OpenAIToolChoiceOption[];
|
|
82
|
+
parallelToolCalls?: boolean;
|
|
83
|
+
reasoningEffort?: OpenAIResponsesEffort[] | false;
|
|
84
|
+
verbosity?: OpenAIResponsesVerbosity[] | false;
|
|
85
|
+
vision?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface GoogleProfile {
|
|
89
|
+
kind: 'google';
|
|
90
|
+
model: string;
|
|
91
|
+
maxTokensCeiling?: number;
|
|
92
|
+
temperature?: NumericRange | false;
|
|
93
|
+
topP?: NumericRange | false;
|
|
94
|
+
topK?: boolean;
|
|
95
|
+
stopSequences?: boolean;
|
|
96
|
+
tools?: boolean;
|
|
97
|
+
functionCallingModes?: GoogleFunctionCallingMode[];
|
|
98
|
+
thinkingBudget?: NumericRange | false;
|
|
99
|
+
responseSchema?: boolean;
|
|
100
|
+
vision?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ModelProfile = AnthropicProfile | OpenAIGenericProfile | OpenAIProProfile | GoogleProfile;
|
|
104
|
+
|
|
105
|
+
export type ProfileFindingSeverity = 'error' | 'warning';
|
|
106
|
+
|
|
107
|
+
export type ProfileFindingCode =
|
|
108
|
+
| 'wrong_type'
|
|
109
|
+
| 'not_accepted'
|
|
110
|
+
| 'below_minimum'
|
|
111
|
+
| 'above_maximum'
|
|
112
|
+
| 'above_ceiling'
|
|
113
|
+
| 'not_in_allowed_set';
|
|
114
|
+
|
|
115
|
+
export interface ProfileFinding {
|
|
116
|
+
severity: ProfileFindingSeverity;
|
|
117
|
+
field: string;
|
|
118
|
+
code: ProfileFindingCode;
|
|
119
|
+
detail: string;
|
|
120
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseSwitchboardRouter } from './routing.js';
|
|
3
|
+
|
|
4
|
+
const valid = {
|
|
5
|
+
user_id: 'eu_123',
|
|
6
|
+
time: '2026-07-16T00:00:00Z',
|
|
7
|
+
idempotency_key: 'idem_1',
|
|
8
|
+
kind: { anthropic: { model: 'claude-sonnet-5', messages: [], max_tokens: 1024 } },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('parseSwitchboardRouter', () => {
|
|
12
|
+
it('parses a valid anthropic router and exposes the envelope', () => {
|
|
13
|
+
const result = parseSwitchboardRouter(valid);
|
|
14
|
+
expect(result.kind).toBe('ok');
|
|
15
|
+
if (result.kind !== 'ok') return;
|
|
16
|
+
expect(result.envelope.userId).toBe('eu_123');
|
|
17
|
+
expect(result.envelope.time).toBe('2026-07-16T00:00:00Z');
|
|
18
|
+
expect(result.envelope.kind).toBe('anthropic');
|
|
19
|
+
expect(result.envelope.idempotencyKey).toBe('idem_1');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rejects a missing idempotency_key — every envelope field is required', () => {
|
|
23
|
+
const { idempotency_key: _omitted, ...withoutKey } = valid;
|
|
24
|
+
expect(parseSwitchboardRouter(withoutKey)).toMatchObject({ kind: 'reject', field: 'idempotency_key' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('forwards the native body opaquely, verbatim', () => {
|
|
28
|
+
const native = {
|
|
29
|
+
model: 'claude-sonnet-5',
|
|
30
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
31
|
+
max_tokens: 64,
|
|
32
|
+
system: [{ type: 'text', text: 's', cache_control: { type: 'ephemeral' } }],
|
|
33
|
+
thinking: { type: 'adaptive', display: 'summarized' },
|
|
34
|
+
output_config: { effort: 'high' },
|
|
35
|
+
};
|
|
36
|
+
const result = parseSwitchboardRouter({ user_id: 'eu', time: '2026-07-16T00:00:00Z', idempotency_key: 'idem_1', kind: { anthropic: native } });
|
|
37
|
+
expect(result.kind).toBe('ok');
|
|
38
|
+
if (result.kind !== 'ok') return;
|
|
39
|
+
expect(result.envelope.request).toBe(native);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects a time that is not a parseable timestamp', () => {
|
|
43
|
+
expect(parseSwitchboardRouter({ ...valid, time: 'not-a-time' })).toMatchObject({ kind: 'reject', field: 'time' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('tolerates unknown envelope fields from a newer client (forward-compatible)', () => {
|
|
47
|
+
const result = parseSwitchboardRouter({ ...valid, some_future_field: true });
|
|
48
|
+
expect(result.kind).toBe('ok');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects an empty user_id', () => {
|
|
52
|
+
expect(parseSwitchboardRouter({ ...valid, user_id: '' })).toMatchObject({ kind: 'reject', field: 'user_id' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects a missing user_id — attribution is always explicit on the envelope', () => {
|
|
56
|
+
const { user_id: _omitted, ...withoutUser } = valid;
|
|
57
|
+
expect(parseSwitchboardRouter(withoutUser)).toMatchObject({ kind: 'reject', field: 'user_id' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rejects a kind carrying more than one provider tag', () => {
|
|
61
|
+
expect(parseSwitchboardRouter({ ...valid, kind: { anthropic: {}, google: {} } })).toMatchObject({
|
|
62
|
+
kind: 'reject',
|
|
63
|
+
field: 'kind',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('parses a google router and exposes the envelope', () => {
|
|
68
|
+
const native = { model: 'gemini-2.5-pro', contents: [{ role: 'user', parts: [{ text: 'hi' }] }] };
|
|
69
|
+
const result = parseSwitchboardRouter({ ...valid, kind: { google: native } });
|
|
70
|
+
expect(result.kind).toBe('ok');
|
|
71
|
+
if (result.kind !== 'ok') return;
|
|
72
|
+
expect(result.envelope.kind).toBe('google');
|
|
73
|
+
expect(result.envelope.request).toBe(native);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects an unknown provider tag', () => {
|
|
77
|
+
expect(parseSwitchboardRouter({ ...valid, kind: { mistral: {} } })).toMatchObject({
|
|
78
|
+
kind: 'reject',
|
|
79
|
+
field: 'kind',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AnthropicMessagesRequest } from './anthropic.js';
|
|
2
|
+
import type { GoogleGenerateContentRequest } from './google.js';
|
|
3
|
+
import type { OpenAIChatRequest } from './openaiGeneric.js';
|
|
4
|
+
import type { OpenAIResponsesRequest } from './openaiPro.js';
|
|
5
|
+
|
|
6
|
+
export type SwitchboardKind = 'anthropic' | 'openai_pro' | 'openai_generic' | 'google';
|
|
7
|
+
|
|
8
|
+
export interface SwitchboardRouter {
|
|
9
|
+
user_id: string;
|
|
10
|
+
time: string;
|
|
11
|
+
idempotency_key: string;
|
|
12
|
+
kind: RouterBody;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RouterBody =
|
|
16
|
+
| { anthropic: AnthropicMessagesRequest }
|
|
17
|
+
| { openai_pro: OpenAIResponsesRequest }
|
|
18
|
+
| { openai_generic: OpenAIChatRequest }
|
|
19
|
+
| { google: GoogleGenerateContentRequest };
|
|
20
|
+
|
|
21
|
+
export interface RouterEnvelope {
|
|
22
|
+
userId: string;
|
|
23
|
+
time: string;
|
|
24
|
+
idempotencyKey: string;
|
|
25
|
+
kind: SwitchboardKind;
|
|
26
|
+
request: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RouterParse =
|
|
30
|
+
| { kind: 'ok'; envelope: RouterEnvelope }
|
|
31
|
+
| { kind: 'reject'; field: string; expected: string };
|
|
32
|
+
|
|
33
|
+
const KINDS: readonly SwitchboardKind[] = ['anthropic', 'openai_pro', 'openai_generic', 'google'];
|
|
34
|
+
|
|
35
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseSwitchboardRouter(body: unknown): RouterParse {
|
|
40
|
+
if (!isPlainObject(body)) return { kind: 'reject', field: 'body', expected: 'a SwitchboardRouter object' };
|
|
41
|
+
|
|
42
|
+
if (typeof body.user_id !== 'string' || body.user_id.length === 0) {
|
|
43
|
+
return { kind: 'reject', field: 'user_id', expected: 'a non-empty string' };
|
|
44
|
+
}
|
|
45
|
+
if (typeof body.time !== 'string' || Number.isNaN(Date.parse(body.time))) {
|
|
46
|
+
return { kind: 'reject', field: 'time', expected: 'an ISO 8601 timestamp' };
|
|
47
|
+
}
|
|
48
|
+
if (typeof body.idempotency_key !== 'string' || body.idempotency_key.length === 0) {
|
|
49
|
+
return { kind: 'reject', field: 'idempotency_key', expected: 'a non-empty string' };
|
|
50
|
+
}
|
|
51
|
+
if (!isPlainObject(body.kind)) {
|
|
52
|
+
return { kind: 'reject', field: 'kind', expected: 'a single-key tagged native body' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tags = Object.keys(body.kind);
|
|
56
|
+
if (tags.length !== 1) {
|
|
57
|
+
return { kind: 'reject', field: 'kind', expected: 'exactly one provider tag' };
|
|
58
|
+
}
|
|
59
|
+
const tag = tags[0];
|
|
60
|
+
if (!KINDS.includes(tag as SwitchboardKind)) {
|
|
61
|
+
return { kind: 'reject', field: 'kind', expected: `one of ${KINDS.join(', ')}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
kind: 'ok',
|
|
66
|
+
envelope: {
|
|
67
|
+
userId: body.user_id,
|
|
68
|
+
time: body.time,
|
|
69
|
+
idempotencyKey: body.idempotency_key,
|
|
70
|
+
kind: tag as SwitchboardKind,
|
|
71
|
+
request: body.kind[tag],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { GoogleProfile, ProfileFinding } from './profile.js';
|
|
3
|
+
import { validateConfig } from './validateConfig.js';
|
|
4
|
+
|
|
5
|
+
const richModel: GoogleProfile = {
|
|
6
|
+
kind: 'google',
|
|
7
|
+
model: 'test-google-rich',
|
|
8
|
+
maxTokensCeiling: 65536,
|
|
9
|
+
temperature: { min: 0, max: 2 },
|
|
10
|
+
topP: { min: 0, max: 1 },
|
|
11
|
+
topK: true,
|
|
12
|
+
stopSequences: true,
|
|
13
|
+
tools: true,
|
|
14
|
+
functionCallingModes: ['AUTO', 'ANY', 'NONE'],
|
|
15
|
+
thinkingBudget: { min: 0, max: 24576 },
|
|
16
|
+
responseSchema: true,
|
|
17
|
+
vision: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const restrictedModel: GoogleProfile = {
|
|
21
|
+
kind: 'google',
|
|
22
|
+
model: 'test-google-restricted',
|
|
23
|
+
maxTokensCeiling: 1000,
|
|
24
|
+
temperature: false,
|
|
25
|
+
topP: false,
|
|
26
|
+
topK: false,
|
|
27
|
+
stopSequences: false,
|
|
28
|
+
tools: false,
|
|
29
|
+
functionCallingModes: ['AUTO'],
|
|
30
|
+
thinkingBudget: false,
|
|
31
|
+
responseSchema: false,
|
|
32
|
+
vision: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const emptyModel: GoogleProfile = { kind: 'google', model: 'test-google-empty' };
|
|
36
|
+
|
|
37
|
+
const base = { model: 'test-google-rich', contents: [{ role: 'user', parts: [{ text: 'hi' }] }] };
|
|
38
|
+
const MEDIA_CONTENTS = [
|
|
39
|
+
{ role: 'user', parts: [{ inlineData: { mimeType: 'image/png', data: 'aGk=' } }] },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function codesFor(findings: ProfileFinding[], field: string): string[] {
|
|
43
|
+
return findings.filter(finding => finding.field === field).map(finding => finding.code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('validateConfig google — clean and boundary requests', () => {
|
|
47
|
+
it('accepts a bare request', () => {
|
|
48
|
+
expect(validateConfig(richModel, base)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('accepts every knob inside the profile', () => {
|
|
52
|
+
const request = {
|
|
53
|
+
...base,
|
|
54
|
+
contents: MEDIA_CONTENTS,
|
|
55
|
+
tools: [{ functionDeclarations: [{ name: 't' }] }],
|
|
56
|
+
toolConfig: { functionCallingConfig: { mode: 'ANY' } },
|
|
57
|
+
generationConfig: {
|
|
58
|
+
maxOutputTokens: 65536,
|
|
59
|
+
temperature: 2,
|
|
60
|
+
topP: 0,
|
|
61
|
+
topK: 40,
|
|
62
|
+
stopSequences: ['stop'],
|
|
63
|
+
responseSchema: { type: 'object' },
|
|
64
|
+
thinkingConfig: { thinkingBudget: 24576 },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
expect(validateConfig(richModel, request)).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('validateConfig google — violations warn', () => {
|
|
72
|
+
const violationCases: Array<{ label: string; patch: Record<string, unknown>; field: string; code: string }> = [
|
|
73
|
+
{ label: 'maxOutputTokens over ceiling', patch: { generationConfig: { maxOutputTokens: 100000 } }, field: 'generationConfig.maxOutputTokens', code: 'above_ceiling' },
|
|
74
|
+
{ label: 'temperature above maximum', patch: { generationConfig: { temperature: 3 } }, field: 'generationConfig.temperature', code: 'above_maximum' },
|
|
75
|
+
{ label: 'thinkingBudget above maximum', patch: { generationConfig: { thinkingConfig: { thinkingBudget: 99999 } } }, field: 'generationConfig.thinkingConfig.thinkingBudget', code: 'above_maximum' },
|
|
76
|
+
{ label: 'undeclared functionCallingConfig mode', patch: { toolConfig: { functionCallingConfig: { mode: 'MYSTERY' } } }, field: 'toolConfig.functionCallingConfig.mode', code: 'not_in_allowed_set' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const violationCase of violationCases) {
|
|
80
|
+
it(`${violationCase.label} warns ${violationCase.code}`, () => {
|
|
81
|
+
const findings = validateConfig(richModel, { ...base, ...violationCase.patch });
|
|
82
|
+
expect(codesFor(findings, violationCase.field)).toEqual([violationCase.code]);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rejectedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
|
|
87
|
+
{ label: 'temperature', patch: { generationConfig: { temperature: 0.5 } }, field: 'generationConfig.temperature' },
|
|
88
|
+
{ label: 'topK', patch: { generationConfig: { topK: 40 } }, field: 'generationConfig.topK' },
|
|
89
|
+
{ label: 'stopSequences', patch: { generationConfig: { stopSequences: ['s'] } }, field: 'generationConfig.stopSequences' },
|
|
90
|
+
{ label: 'responseSchema', patch: { generationConfig: { responseSchema: {} } }, field: 'generationConfig.responseSchema' },
|
|
91
|
+
{ label: 'thinkingBudget', patch: { generationConfig: { thinkingConfig: { thinkingBudget: 1024 } } }, field: 'generationConfig.thinkingConfig.thinkingBudget' },
|
|
92
|
+
{ label: 'tools', patch: { tools: [{ functionDeclarations: [{ name: 't' }] }] }, field: 'tools' },
|
|
93
|
+
{ label: 'media content', patch: { contents: MEDIA_CONTENTS }, field: 'contents' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const rejectedCase of rejectedCases) {
|
|
97
|
+
it(`${rejectedCase.label} on a restricted model warns not_accepted`, () => {
|
|
98
|
+
const findings = validateConfig(restrictedModel, { ...base, ...rejectedCase.patch });
|
|
99
|
+
expect(codesFor(findings, rejectedCase.field)).toEqual(['not_accepted']);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it('undeclared mode on the restricted model warns not_in_allowed_set', () => {
|
|
104
|
+
const findings = validateConfig(restrictedModel, { ...base, toolConfig: { functionCallingConfig: { mode: 'ANY' } } });
|
|
105
|
+
expect(codesFor(findings, 'toolConfig.functionCallingConfig.mode')).toEqual(['not_in_allowed_set']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('validateConfig google — format errors and fail-open', () => {
|
|
110
|
+
const malformedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
|
|
111
|
+
{ label: 'non-object generationConfig', patch: { generationConfig: 'hot' }, field: 'generationConfig' },
|
|
112
|
+
{ label: 'non-numeric temperature', patch: { generationConfig: { temperature: 'hot' } }, field: 'generationConfig.temperature' },
|
|
113
|
+
{ label: 'non-object thinkingConfig', patch: { generationConfig: { thinkingConfig: 'deep' } }, field: 'generationConfig.thinkingConfig' },
|
|
114
|
+
{ label: 'non-numeric thinkingBudget', patch: { generationConfig: { thinkingConfig: { thinkingBudget: 'lots' } } }, field: 'generationConfig.thinkingConfig.thinkingBudget' },
|
|
115
|
+
{ label: 'non-object toolConfig', patch: { toolConfig: 'auto' }, field: 'toolConfig' },
|
|
116
|
+
{ label: 'non-string mode', patch: { toolConfig: { functionCallingConfig: { mode: 7 } } }, field: 'toolConfig.functionCallingConfig.mode' },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const malformedCase of malformedCases) {
|
|
120
|
+
it(`${malformedCase.label} errors even on an empty profile`, () => {
|
|
121
|
+
const findings = validateConfig(emptyModel, { ...base, ...malformedCase.patch });
|
|
122
|
+
expect(findings).toEqual([
|
|
123
|
+
expect.objectContaining({ severity: 'error', field: malformedCase.field, code: 'wrong_type' }),
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('an empty profile never warns on extreme values', () => {
|
|
129
|
+
const request = {
|
|
130
|
+
...base,
|
|
131
|
+
contents: MEDIA_CONTENTS,
|
|
132
|
+
generationConfig: { maxOutputTokens: 9000000, temperature: 42, thinkingConfig: { thinkingBudget: 999999 } },
|
|
133
|
+
toolConfig: { functionCallingConfig: { mode: 'MYSTERY' } },
|
|
134
|
+
};
|
|
135
|
+
expect(validateConfig(emptyModel, request)).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { OpenAIGenericProfile, OpenAIProProfile, ProfileFinding } from './profile.js';
|
|
3
|
+
import { validateConfig } from './validateConfig.js';
|
|
4
|
+
|
|
5
|
+
const richChatModel: OpenAIGenericProfile = {
|
|
6
|
+
kind: 'openai_generic',
|
|
7
|
+
model: 'test-chat-rich',
|
|
8
|
+
maxTokensCeiling: 32000,
|
|
9
|
+
temperature: { min: 0, max: 2 },
|
|
10
|
+
topP: { min: 0, max: 1 },
|
|
11
|
+
frequencyPenalty: { min: -2, max: 2 },
|
|
12
|
+
presencePenalty: { min: -2, max: 2 },
|
|
13
|
+
tools: true,
|
|
14
|
+
toolChoice: ['auto', 'none', 'required', 'function'],
|
|
15
|
+
parallelToolCalls: true,
|
|
16
|
+
responseFormat: true,
|
|
17
|
+
reasoningEffort: ['low', 'medium', 'high'],
|
|
18
|
+
verbosity: ['low', 'medium', 'high'],
|
|
19
|
+
vision: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const restrictedChatModel: OpenAIGenericProfile = {
|
|
23
|
+
kind: 'openai_generic',
|
|
24
|
+
model: 'test-chat-restricted',
|
|
25
|
+
maxTokensCeiling: 1000,
|
|
26
|
+
temperature: false,
|
|
27
|
+
topP: false,
|
|
28
|
+
frequencyPenalty: false,
|
|
29
|
+
presencePenalty: false,
|
|
30
|
+
tools: false,
|
|
31
|
+
toolChoice: ['auto'],
|
|
32
|
+
parallelToolCalls: false,
|
|
33
|
+
responseFormat: false,
|
|
34
|
+
reasoningEffort: false,
|
|
35
|
+
verbosity: false,
|
|
36
|
+
vision: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const emptyChatModel: OpenAIGenericProfile = { kind: 'openai_generic', model: 'test-chat-empty' };
|
|
40
|
+
|
|
41
|
+
const richProModel: OpenAIProProfile = {
|
|
42
|
+
kind: 'openai_pro',
|
|
43
|
+
model: 'test-pro-rich',
|
|
44
|
+
maxTokensCeiling: 64000,
|
|
45
|
+
temperature: { min: 0, max: 2 },
|
|
46
|
+
topP: { min: 0, max: 1 },
|
|
47
|
+
tools: true,
|
|
48
|
+
toolChoice: ['auto', 'none', 'required', 'function'],
|
|
49
|
+
parallelToolCalls: true,
|
|
50
|
+
reasoningEffort: ['low', 'medium', 'high', 'xhigh'],
|
|
51
|
+
verbosity: ['low', 'medium', 'high'],
|
|
52
|
+
vision: true,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const restrictedProModel: OpenAIProProfile = {
|
|
56
|
+
kind: 'openai_pro',
|
|
57
|
+
model: 'test-pro-restricted',
|
|
58
|
+
temperature: false,
|
|
59
|
+
topP: false,
|
|
60
|
+
tools: false,
|
|
61
|
+
toolChoice: ['auto'],
|
|
62
|
+
parallelToolCalls: false,
|
|
63
|
+
reasoningEffort: false,
|
|
64
|
+
verbosity: false,
|
|
65
|
+
vision: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const chatBase = { model: 'test-chat-rich', messages: [{ role: 'user', content: 'hi' }] };
|
|
69
|
+
const proBase = { model: 'test-pro-rich', input: 'hi' };
|
|
70
|
+
|
|
71
|
+
const IMAGE_CHAT_MESSAGES = [
|
|
72
|
+
{ role: 'user', content: [{ type: 'image_url', image_url: { url: 'https://example.com/i.png' } }] },
|
|
73
|
+
];
|
|
74
|
+
const IMAGE_PRO_INPUT = [
|
|
75
|
+
{ type: 'message', role: 'user', content: [{ type: 'input_image', image_url: 'https://example.com/i.png' }] },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function codesFor(findings: ProfileFinding[], field: string): string[] {
|
|
79
|
+
return findings.filter(finding => finding.field === field).map(finding => finding.code);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('validateConfig openai_generic — clean and boundary requests', () => {
|
|
83
|
+
it('accepts a bare request', () => {
|
|
84
|
+
expect(validateConfig(richChatModel, chatBase)).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('accepts every knob inside the profile', () => {
|
|
88
|
+
const request = {
|
|
89
|
+
...chatBase,
|
|
90
|
+
max_completion_tokens: 32000,
|
|
91
|
+
temperature: 2,
|
|
92
|
+
top_p: 0,
|
|
93
|
+
frequency_penalty: -2,
|
|
94
|
+
presence_penalty: 2,
|
|
95
|
+
tools: [{ type: 'function', function: { name: 't' } }],
|
|
96
|
+
tool_choice: 'auto',
|
|
97
|
+
parallel_tool_calls: true,
|
|
98
|
+
response_format: { type: 'json_object' },
|
|
99
|
+
reasoning_effort: 'high',
|
|
100
|
+
verbosity: 'low',
|
|
101
|
+
messages: IMAGE_CHAT_MESSAGES,
|
|
102
|
+
};
|
|
103
|
+
expect(validateConfig(richChatModel, request)).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('accepts object-form tool_choice naming a function', () => {
|
|
107
|
+
const request = { ...chatBase, tool_choice: { type: 'function', function: { name: 't' } } };
|
|
108
|
+
expect(validateConfig(richChatModel, request)).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('validateConfig openai_generic — violations warn', () => {
|
|
113
|
+
const violationCases: Array<{ label: string; patch: Record<string, unknown>; field: string; code: string }> = [
|
|
114
|
+
{ label: 'max_completion_tokens over ceiling', patch: { max_completion_tokens: 64000 }, field: 'max_completion_tokens', code: 'above_ceiling' },
|
|
115
|
+
{ label: 'legacy max_tokens over ceiling', patch: { max_tokens: 64000 }, field: 'max_tokens', code: 'above_ceiling' },
|
|
116
|
+
{ label: 'temperature above maximum', patch: { temperature: 3 }, field: 'temperature', code: 'above_maximum' },
|
|
117
|
+
{ label: 'frequency_penalty below minimum', patch: { frequency_penalty: -3 }, field: 'frequency_penalty', code: 'below_minimum' },
|
|
118
|
+
{ label: 'undeclared reasoning_effort', patch: { reasoning_effort: 'xhigh' }, field: 'reasoning_effort', code: 'not_in_allowed_set' },
|
|
119
|
+
{ label: 'undeclared verbosity', patch: { verbosity: 'terse' }, field: 'verbosity', code: 'not_in_allowed_set' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const violationCase of violationCases) {
|
|
123
|
+
it(`${violationCase.label} warns ${violationCase.code}`, () => {
|
|
124
|
+
const findings = validateConfig(richChatModel, { ...chatBase, ...violationCase.patch });
|
|
125
|
+
expect(codesFor(findings, violationCase.field)).toEqual([violationCase.code]);
|
|
126
|
+
expect(findings.every(finding => finding.severity === 'warning')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const rejectedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
|
|
131
|
+
{ label: 'temperature', patch: { temperature: 0.5 }, field: 'temperature' },
|
|
132
|
+
{ label: 'top_p', patch: { top_p: 0.5 }, field: 'top_p' },
|
|
133
|
+
{ label: 'tools', patch: { tools: [{ type: 'function', function: { name: 't' } }] }, field: 'tools' },
|
|
134
|
+
{ label: 'parallel_tool_calls', patch: { parallel_tool_calls: true }, field: 'parallel_tool_calls' },
|
|
135
|
+
{ label: 'response_format', patch: { response_format: { type: 'json_object' } }, field: 'response_format' },
|
|
136
|
+
{ label: 'reasoning_effort', patch: { reasoning_effort: 'high' }, field: 'reasoning_effort' },
|
|
137
|
+
{ label: 'vision', patch: { messages: IMAGE_CHAT_MESSAGES }, field: 'messages' },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const rejectedCase of rejectedCases) {
|
|
141
|
+
it(`${rejectedCase.label} on a restricted model warns not_accepted`, () => {
|
|
142
|
+
const findings = validateConfig(restrictedChatModel, { ...chatBase, ...rejectedCase.patch });
|
|
143
|
+
expect(codesFor(findings, rejectedCase.field)).toEqual(['not_accepted']);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it('undeclared tool_choice warns not_in_allowed_set', () => {
|
|
148
|
+
const findings = validateConfig(restrictedChatModel, { ...chatBase, tool_choice: 'required' });
|
|
149
|
+
expect(codesFor(findings, 'tool_choice')).toEqual(['not_in_allowed_set']);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('validateConfig openai_generic — format errors and fail-open', () => {
|
|
154
|
+
const malformedCases: Array<{ label: string; patch: Record<string, unknown>; field: string }> = [
|
|
155
|
+
{ label: 'non-numeric temperature', patch: { temperature: 'hot' }, field: 'temperature' },
|
|
156
|
+
{ label: 'non-boolean parallel_tool_calls', patch: { parallel_tool_calls: 'yes' }, field: 'parallel_tool_calls' },
|
|
157
|
+
{ label: 'non-object response_format', patch: { response_format: 'json' }, field: 'response_format' },
|
|
158
|
+
{ label: 'non-string reasoning_effort', patch: { reasoning_effort: 3 }, field: 'reasoning_effort' },
|
|
159
|
+
{ label: 'numeric tool_choice', patch: { tool_choice: 7 }, field: 'tool_choice' },
|
|
160
|
+
{ label: 'object tool_choice without string type', patch: { tool_choice: { type: 7 } }, field: 'tool_choice.type' },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const malformedCase of malformedCases) {
|
|
164
|
+
it(`${malformedCase.label} errors even on an empty profile`, () => {
|
|
165
|
+
const findings = validateConfig(emptyChatModel, { ...chatBase, ...malformedCase.patch });
|
|
166
|
+
expect(findings).toEqual([
|
|
167
|
+
expect.objectContaining({ severity: 'error', field: malformedCase.field, code: 'wrong_type' }),
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
it('an empty profile never warns on extreme values', () => {
|
|
173
|
+
const request = {
|
|
174
|
+
...chatBase,
|
|
175
|
+
max_completion_tokens: 9_000_000,
|
|
176
|
+
temperature: 42,
|
|
177
|
+
reasoning_effort: 'nonexistent',
|
|
178
|
+
tool_choice: 'mystery',
|
|
179
|
+
messages: IMAGE_CHAT_MESSAGES,
|
|
180
|
+
};
|
|
181
|
+
expect(validateConfig(emptyChatModel, request)).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('validateConfig openai_pro', () => {
|
|
186
|
+
it('accepts every knob inside the profile', () => {
|
|
187
|
+
const request = {
|
|
188
|
+
...proBase,
|
|
189
|
+
max_output_tokens: 64000,
|
|
190
|
+
temperature: 0,
|
|
191
|
+
top_p: 1,
|
|
192
|
+
tools: [{ type: 'function', name: 't' }],
|
|
193
|
+
tool_choice: { type: 'function', name: 't' },
|
|
194
|
+
parallel_tool_calls: true,
|
|
195
|
+
reasoning: { effort: 'xhigh' },
|
|
196
|
+
text: { verbosity: 'high' },
|
|
197
|
+
input: IMAGE_PRO_INPUT,
|
|
198
|
+
};
|
|
199
|
+
expect(validateConfig(richProModel, request)).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('warns above the max_output_tokens ceiling', () => {
|
|
203
|
+
const findings = validateConfig(richProModel, { ...proBase, max_output_tokens: 128000 });
|
|
204
|
+
expect(codesFor(findings, 'max_output_tokens')).toEqual(['above_ceiling']);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('warns on an undeclared nested reasoning effort', () => {
|
|
208
|
+
const findings = validateConfig(richProModel, { ...proBase, reasoning: { effort: 'max' } });
|
|
209
|
+
expect(codesFor(findings, 'reasoning.effort')).toEqual(['not_in_allowed_set']);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('warns not_accepted on nested effort for a restricted model', () => {
|
|
213
|
+
const findings = validateConfig(restrictedProModel, { ...proBase, reasoning: { effort: 'high' } });
|
|
214
|
+
expect(codesFor(findings, 'reasoning.effort')).toEqual(['not_accepted']);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('warns not_accepted on image input for a model without vision', () => {
|
|
218
|
+
const findings = validateConfig(restrictedProModel, { ...proBase, input: IMAGE_PRO_INPUT });
|
|
219
|
+
expect(codesFor(findings, 'input')).toEqual(['not_accepted']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('errors on a non-object reasoning', () => {
|
|
223
|
+
const findings = validateConfig(richProModel, { ...proBase, reasoning: 'hard' });
|
|
224
|
+
expect(findings).toEqual([
|
|
225
|
+
expect.objectContaining({ severity: 'error', field: 'reasoning', code: 'wrong_type' }),
|
|
226
|
+
]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('errors on a non-string nested verbosity', () => {
|
|
230
|
+
const findings = validateConfig(richProModel, { ...proBase, text: { verbosity: 3 } });
|
|
231
|
+
expect(findings).toEqual([
|
|
232
|
+
expect.objectContaining({ severity: 'error', field: 'text.verbosity', code: 'wrong_type' }),
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('rejects a non-object request', () => {
|
|
237
|
+
expect(validateConfig(richProModel, 'not an object')).toEqual([
|
|
238
|
+
expect.objectContaining({ severity: 'error', field: 'request', code: 'wrong_type' }),
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
});
|