@crossworks/voice-client 0.232.207 → 0.232.210

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossworks/voice-client",
3
- "version": "0.232.207",
3
+ "version": "0.232.210",
4
4
  "description": "Browser-safe surface of the voice/model layer — provider catalogue, model catalogs, audio tags, and the adapter type/metadata contract. Zero deps by design: nothing here may reach the network adapters or node builtins (the jackdaw-repo-split P0 boundary).",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -15,6 +15,7 @@
15
15
  import type { Provider, ProviderCapability, ProviderId } from '../providers';
16
16
  import type {
17
17
  ChatDispatcher,
18
+ DecisionDispatcher,
18
19
  EmbeddingDispatcher,
19
20
  ImageGenDispatcher,
20
21
  SttDispatcher,
@@ -29,8 +30,10 @@ const STT = new Map<ProviderId, SttDispatcher>();
29
30
  const VISION = new Map<ProviderId, VisionDispatcher>();
30
31
  const IMAGE_GEN = new Map<ProviderId, ImageGenDispatcher>();
31
32
  const EMBEDDING = new Map<ProviderId, EmbeddingDispatcher>();
33
+ const DECISION = new Map<ProviderId, DecisionDispatcher>();
32
34
 
33
- export type WiredCapability = 'chat' | 'tts' | 'stt' | 'vision' | 'image_gen' | 'embedding';
35
+ export type WiredCapability =
36
+ 'chat' | 'tts' | 'stt' | 'vision' | 'image_gen' | 'embedding' | 'decision';
34
37
 
35
38
  /**
36
39
  * STATIC mirror of which providers have a registered adapter, per capability —
@@ -70,6 +73,8 @@ export const WIRED_PROVIDERS: Record<WiredCapability, ReadonlySet<ProviderId>> =
70
73
  vision: new Set<ProviderId>(['openai', 'anthropic', 'google', 'xai', 'openrouter']),
71
74
  image_gen: new Set<ProviderId>(['openrouter', 'openai', 'xai', 'google', 'huggingface']),
72
75
  embedding: new Set<ProviderId>(['openrouter', 'openai', 'google', 'mistral', 'cohere', 'local']),
76
+ // Typed decisions: only OpenRouter's alpha decisions endpoint today.
77
+ decision: new Set<ProviderId>(['openrouter']),
73
78
  };
74
79
 
75
80
  function mapFor(capability: WiredCapability): ReadonlyMap<ProviderId, unknown> {
@@ -80,6 +85,7 @@ function mapFor(capability: WiredCapability): ReadonlyMap<ProviderId, unknown> {
80
85
  vision: VISION,
81
86
  image_gen: IMAGE_GEN,
82
87
  embedding: EMBEDDING,
88
+ decision: DECISION,
83
89
  }[capability];
84
90
  }
85
91
 
@@ -189,6 +195,19 @@ export function listEmbeddingAdapters(): EmbeddingDispatcher[] {
189
195
  return Array.from(EMBEDDING.values());
190
196
  }
191
197
 
198
+ // ─── Decision (typed answers, no prose) ──────────────────────────────
199
+
200
+ export function registerDecisionAdapter(adapter: DecisionDispatcher): void {
201
+ DECISION.set(adapter.providerId, adapter);
202
+ }
203
+
204
+ /** No retry wrapper on purpose: a decision sits IN FRONT of a call the caller
205
+ * will make anyway, so a failed decision is simply "no decision" — retrying
206
+ * would spend the latency budget the caller was trying to save. */
207
+ export function getDecisionAdapter(providerId: string): DecisionDispatcher | null {
208
+ return DECISION.get(providerId as ProviderId) ?? null;
209
+ }
210
+
192
211
  // ─── Capability check (used by UI to derive `wired` flag) ────────────
193
212
 
194
213
  /**
@@ -220,10 +239,7 @@ export function findAdapterCatalogDrift(
220
239
  const problems: string[] = [];
221
240
  const catalogById = new Map(providers.map((p) => [p.id as string, p.capabilities]));
222
241
 
223
- function check(
224
- label: 'chat' | 'tts' | 'stt' | 'vision' | 'image_gen' | 'embedding',
225
- registry: Map<ProviderId, { adapterName: string }>,
226
- ): void {
242
+ function check(label: WiredCapability, registry: Map<ProviderId, { adapterName: string }>): void {
227
243
  for (const [providerId, adapter] of registry) {
228
244
  const caps = catalogById.get(providerId);
229
245
  if (!caps) {
@@ -247,6 +263,7 @@ export function findAdapterCatalogDrift(
247
263
  check('vision', VISION);
248
264
  check('image_gen', IMAGE_GEN);
249
265
  check('embedding', EMBEDDING);
266
+ check('decision', DECISION);
250
267
 
251
268
  return problems;
252
269
  }
@@ -864,3 +864,65 @@ export interface EmbeddingDispatcher extends AdapterMeta {
864
864
  * AND as a last-resort if discovery errors. */
865
865
  staticCatalog?(): readonly EmbeddingModelInfo[];
866
866
  }
867
+
868
+ // ─── Decision (typed answers, no prose) ─────────────────────────────
869
+
870
+ /** One question for a decision model. Three shapes, mirroring TypeSafe's
871
+ * primitives: `choice` picks one key from `criteria`; `score` places the
872
+ * state on an ORDERED rubric (index 0 = lowest); `noul` is a yes/no whose
873
+ * answer is the probability of "yes". Write `instructions` as one direct
874
+ * question and name the state fields it refers to in backticks — the model
875
+ * reads literally, so contrastive criteria ("not for …") pay off. */
876
+ export type DecisionQuestion =
877
+ | { type: 'choice'; instructions: string; criteria: Record<string, string> }
878
+ | { type: 'score'; instructions: string; criteria: readonly string[] }
879
+ | { type: 'noul'; instructions: string; criteria?: { true?: string; false?: string } };
880
+
881
+ /** One answer. `confidence` (0-1) is the SHAPE of the distribution — 1 when
882
+ * all mass sits on one option, 0 when it is flat — and is the number the
883
+ * caller gates on; `probabilities` say WHICH options were likely. A `noul`
884
+ * has no separate confidence: its probability is the whole answer. */
885
+ export type DecisionAnswer =
886
+ | {
887
+ type: 'choice';
888
+ choice: string;
889
+ confidence: number;
890
+ probabilities: Record<string, number>;
891
+ }
892
+ | {
893
+ type: 'score';
894
+ /** Probability-weighted mean over the rubric indexes (fractional). */
895
+ score: number;
896
+ confidence: number;
897
+ probabilities: Record<string, number>;
898
+ }
899
+ | { type: 'noul'; probability: number };
900
+
901
+ export interface DecisionOptions {
902
+ apiKey: string;
903
+ model: string;
904
+ /** The evidence: a string, a JSON object (preferred — every part has a
905
+ * name the questions can point at) or an array. Text only. */
906
+ state: unknown;
907
+ questions: Record<string, DecisionQuestion>;
908
+ /** Ask the provider for zero-data-retention routing and no data
909
+ * collection. The state can hold the owner's documents. */
910
+ zeroDataRetention?: boolean;
911
+ /** Hard ceiling; the adapter aborts and throws past it. */
912
+ timeoutMs?: number;
913
+ }
914
+
915
+ export interface DecisionResult {
916
+ answers: Record<string, DecisionAnswer>;
917
+ /** Echo of the model the provider actually served. */
918
+ model: string;
919
+ tokensIn?: number;
920
+ tokensOut?: number;
921
+ /** Provider-reported USD cost (OpenRouter `usage.cost`). */
922
+ reportedCostUsd?: number;
923
+ }
924
+
925
+ export interface DecisionDispatcher extends AdapterMeta {
926
+ /** One evaluation: all questions answered in one parallel pass. */
927
+ decide(opts: DecisionOptions): Promise<DecisionResult>;
928
+ }
package/src/providers.ts CHANGED
@@ -48,7 +48,17 @@ export type ProviderId =
48
48
  *
49
49
  * When a new ai_workers kind ships, add its capability here too so
50
50
  * the provider filter knows which providers to expose for that kind. */
51
- export type ProviderCapability = 'chat' | 'embedding' | 'tts' | 'stt' | 'vision' | 'image_gen';
51
+ export type ProviderCapability =
52
+ | 'chat'
53
+ | 'embedding'
54
+ | 'tts'
55
+ | 'stt'
56
+ | 'vision'
57
+ | 'image_gen'
58
+ /** Typed decisions (choice / score / yes-no with probabilities, no prose)
59
+ * — TypeSafe Jev through OpenRouter's decisions endpoint. Not chat-shaped:
60
+ * its own dispatcher, its own endpoint. */
61
+ | 'decision';
52
62
 
53
63
  export type Provider = {
54
64
  id: ProviderId;
@@ -82,7 +92,7 @@ export const SUPPORTED_PROVIDERS: readonly Provider[] = [
82
92
  label: 'OpenRouter',
83
93
  description:
84
94
  'Aggregator covering OpenAI, Anthropic, Google, Mistral, DeepSeek, and most open models behind one key — plus audio (TTS/STT) and image generation. One key powers chat, memory, voice, and images.',
85
- capabilities: ['chat', 'embedding', 'vision', 'tts', 'stt', 'image_gen'],
95
+ capabilities: ['chat', 'embedding', 'vision', 'tts', 'stt', 'image_gen', 'decision'],
86
96
  signupUrl: 'https://openrouter.ai/keys',
87
97
  docsUrl: 'https://openrouter.ai/docs',
88
98
  isAggregator: true,
@@ -259,4 +269,7 @@ export const CAPABILITY_FOR_KIND: Record<string, ProviderCapability> = {
259
269
  // The suggester runs a plain chat-completion to propose one follow-up
260
270
  // question after a turn; same 'chat' capability as the narrator.
261
271
  suggester: 'chat',
272
+ // The decider is the first kind whose output is neither prose nor media: a
273
+ // typed-decision model behind its own endpoint, so its own capability.
274
+ decider: 'decision',
262
275
  };