@x12i/ai-profiles 3.1.0 → 3.2.0

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 (2) hide show
  1. package/README.md +108 -40
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Resolve AI profile names into concrete model choices. The package ships JSON in `src/data/` as the offline fallback. By default, registry data is fetched from public assets, validated, and cached in memory.
4
4
 
5
- > **v3.0** removes legacy resolution inputs: shortcuts, bare profiles, and aliases. Only four forms are accepted — `profile/choice`, vendor slug, gateway wire, and `{ provider, modelId }`. See [CHANGELOG.md](CHANGELOG.md).
5
+ > **v3.2** adds **`normalizeInvokeModel`**, **`resolveAndNormalizeInvokeModel`**, and **`resolveAndNormalizeBundledInvokeModel`** for gateway invoke wire shape. Downstream repos should pin **`"@x12i/ai-profiles": "^3.2.0"`**.
6
+ >
7
+ > **v3.0** removes legacy resolution inputs: shortcuts, bare profiles, and aliases. Only four resolution forms are accepted — `profile/choice`, vendor slug, gateway wire, and `{ provider, modelId }`. See [CHANGELOG.md](CHANGELOG.md).
6
8
  >
7
9
  > **v2.0** (still relevant): every `resolveAIProfile` call must pass `catalogLane` (`"text"`, `"image"`, …). Profile keys `deepseek` and `summarization` are gone — use `vol` and `sum`.
8
10
 
@@ -35,6 +37,36 @@ Every profile has a **`defaultChoice`** (recommended) and typically **two altern
35
37
 
36
38
  Tasks should declare **requirements** as explicit `profile/choice` keys (e.g. `cheap/default`), not model names. Use discovery APIs (`listAIProfiles`, `resolveAIProfileByTags`) to pick a profile when the caller does not already have a composite key.
37
39
 
40
+ ### Graph and stored model config
41
+
42
+ Graph JSON, studio defaults, and run overrides store **profile slot strings only** — never concrete provider model ids. Each task uses a complete triple:
43
+
44
+ | Slot | Example | Phase |
45
+ |------|---------|-------|
46
+ | `preActionModel` | `deep/default` | PRE (xynthesis / planning) |
47
+ | `skillModel` | `balanced/default` | MAIN (skill gateway) |
48
+ | `postActionModel` | `cheap/default` | POST (xynthesis / wrap-up) |
49
+
50
+ Canonical storage form is **`profile/choice`** (separator `/`). Use **`formatProfileChoiceKey(profile, choice)`** when writing slots and **`isKnownProfileChoice(input)`** (or **`matchStrictProfileInput`**) before persist or execute. Always include the choice segment — even for the profile default (`cheap/default`, not bare `cheap`).
51
+
52
+ ```json
53
+ "modelConfig": {
54
+ "cases": [
55
+ {
56
+ "modelConfig": {
57
+ "preActionModel": "cyber/default",
58
+ "skillModel": "audit/default",
59
+ "postActionModel": "cheap/default"
60
+ }
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ **At execute time**, hosts pass the stored alias triple unchanged; each phase resolves independently via `resolveAIProfile` (or `resolveAndNormalizeInvokeModel` at the gateway boundary). Do **not** pre-resolve aliases to concrete ids in graph JSON, `runtime.modelConfig`, or `aliasConfig`.
67
+
68
+ **Forbidden in stored config:** legacy keys (`xynthesisModel`, `postStrategyModel`), legacy aliases (`default`, `strong`, `weak`), bare profile keys (`cheap`), concrete slugs (`openai/gpt-4o-mini`), and legacy `@` composites (`cheap@anthropic_cheap` — migrate to `cheap/anthropic_cheap` on save).
69
+
38
70
  ### Profile catalog (bundled)
39
71
 
40
72
  Profiles are grouped by **what kind of work** they represent—not only by price/quality tier. Several axes are orthogonal (structured output, agents, research, local inference).
@@ -213,7 +245,11 @@ npm install @x12i/ai-profiles
213
245
  | `loadBundledModelsCatalog()` | Yes | Package-shipped `models-catalog.json` only |
214
246
  | `loadBundledModelsRegistry()` | Yes | Package-shipped `models-registry.json` (linkage index) |
215
247
  | `listAIProfileChoices(options?)` | No (async) | List explicit `profile/choice` keys for strict pickers and stored config |
248
+ | `formatProfileChoiceKey(profile, choice)` | Yes | Build canonical slot key (`cheap/default`) |
249
+ | `parseProfileChoiceKey(input)` | Yes | Parse `profile/choice` (accepts `\` as separator on input) |
250
+ | `matchStrictProfileInput(registry, input)` | Yes | Resolve input to profile+choice or `invalidChoice` |
216
251
  | `isKnownProfileChoice(input)` | Yes (bundled only) | Whether input is a known valid `profile/choice` key |
252
+ | `isKnownProfileChoiceInput(registry, input)` | Yes | Same as above with an explicit registry |
217
253
  | `getBundledRegistry()` | Yes | Sync bundled profiles registry (no network) |
218
254
  | `resolveBundledAIProfile(input, options)` | Yes | Strict `profile/choice` → model, pricing, runtime (`source: "bundled"`) |
219
255
  | `resolveBundledInput(input, options)` | Yes | Unified resolver: `profile/choice` → registry; vendor slug / gateway wire / `{ provider, modelId }` → catalog |
@@ -230,7 +266,7 @@ npm install @x12i/ai-profiles
230
266
  | `listProfileSecondaries(profileKey, registry)` | Yes | List per-choice `secondary` declarations on a profile |
231
267
  | `resolveAIProfileByTags(tags, options?)` | No (async) | Pick best profile by tag overlap (partial match OK), then resolve |
232
268
  | `scoreProfileTags` / `rankProfilesByTags` | Yes | Tag overlap scoring for discovery and custom UIs |
233
- | `AIProfilesError` | — | Typed errors (`UNKNOWN_PROFILE`, …) |
269
+ | `AIProfilesError` / `AIProfilesErrorCode` | — | Typed errors (`UNKNOWN_PROFILE`, `INVOKE_MODEL_ALIAS_AT_GATEWAY`, `INVOKE_MODEL_SHAPE_INVALID`, …) |
234
270
 
235
271
  ### Model identity (vendor + model strings)
236
272
 
@@ -437,40 +473,65 @@ ensureConcreteModel("google/gemini-2.5-flash-lite");
437
473
 
438
474
  ## Invoke wire shape (gateway / HTTP)
439
475
 
440
- Downstream packages (`ai-tasks`, `ai-skills`, `ai-gateway`, `xynthesis`, `optimixer`) must agree on what is sent on the wire **after** semantic resolution:
476
+ Downstream packages (`ai-tasks`, `ai-skills`, `ai-gateway`, `xynthesis`, `optimixer`) must agree on what is sent on the wire **after** semantic resolution. **Stored graph config** uses `profile/choice` aliases; **gateway / HTTP** uses the invoke tuple below.
477
+
478
+ ### Display slug
479
+
480
+ A **display slug** is a single string with an `openrouter/` prefix for human-readable UI and logs, e.g. `openrouter/deepseek/deepseek-v4-pro`. Studio pickers, plan previews, and Activix run logs may show this form. It is **not** the shape passed to `ai-gateway` catalog lookup or OpenRouter HTTP — normalize before those boundaries.
441
481
 
442
- | Shape | Example | Use |
443
- |-------|---------|-----|
444
- | **Display slug** | `openrouter/deepseek/deepseek-v4-pro` | Studio UI, plan logs, human preview only |
445
- | **Invoke tuple** | `{ provider: "openrouter", model: "deepseek/deepseek-v4-pro" }` | `ai-gateway` catalog lookup, OpenRouter HTTP, funcx PRE/POST |
482
+ ### Invoke tuple
446
483
 
447
- Call **`normalizeInvokeModel`** at every boundary before catalog lookup or HTTP (idempotent). Profile intent still goes through **`resolveAIProfile`** first (or **`resolveAndNormalizeInvokeModel`** for the combined path).
484
+ An **invoke tuple** is `{ provider, model, routing }` where `provider` is `"openrouter"` (or a vendor for direct routing) and `model` is the **bare** vendor/model slug without a gateway prefix, e.g. `{ provider: "openrouter", model: "deepseek/deepseek-v4-pro", routing: "openrouter" }`. This is what `ai-gateway` uses for catalog lookup and what OpenRouter HTTP expects.
485
+
486
+ Call **`normalizeInvokeModel`** at every gateway boundary (idempotent). Profile intent still goes through **`resolveAIProfile`** first, or use **`resolveAndNormalizeInvokeModel`** / **`resolveAndNormalizeBundledInvokeModel`** for the combined path.
487
+
488
+ | Export | Sync? | Purpose |
489
+ |--------|-------|---------|
490
+ | `normalizeInvokeModel(input, options?)` | Yes | Display slug or invoke object → canonical invoke tuple; rejects unresolved `profile/choice` |
491
+ | `resolveAndNormalizeInvokeModel(alias, options)` | No (async) | `resolveAIProfile` → `normalizeInvokeModel` |
492
+ | `resolveAndNormalizeBundledInvokeModel(alias, options)` | Yes | Bundled `resolveAIProfile` → `normalizeInvokeModel` (no network) |
448
493
 
449
494
  ```ts
450
495
  import {
451
496
  normalizeInvokeModel,
452
497
  resolveAndNormalizeInvokeModel,
498
+ resolveAndNormalizeBundledInvokeModel,
453
499
  } from "@x12i/ai-profiles";
454
500
 
455
- // Display slug → invoke tuple (MAIN handoff fix)
501
+ // Display slug → invoke tuple (string or { model } object)
456
502
  normalizeInvokeModel("openrouter/deepseek/deepseek-v4-pro");
503
+ normalizeInvokeModel({ model: "openrouter/deepseek/deepseek-v4-pro" });
457
504
  // → { provider: "openrouter", model: "deepseek/deepseek-v4-pro", routing: "openrouter", displaySlug: "..." }
458
505
 
459
- // Already-correct tuple → unchanged
506
+ // Already-correct tuple → unchanged (idempotent)
460
507
  normalizeInvokeModel({ provider: "openrouter", model: "deepseek/deepseek-v4-pro" });
461
508
 
462
- // Alias at gateway without resolution → INVOKE_MODEL_ALIAS_AT_GATEWAY
463
- normalizeInvokeModel("cyber/deep_forensics"); // throws
509
+ // Unresolved profile/choice at gateway → INVOKE_MODEL_ALIAS_AT_GATEWAY
510
+ normalizeInvokeModel("cheap/default");
511
+ normalizeInvokeModel({ model: "cheap/default" }); // same error
464
512
 
465
- // Profile/choice → resolve, then normalize
513
+ // Profile/choice → resolve, then normalize (gateway catalog-lookup tuple)
466
514
  await resolveAndNormalizeInvokeModel("cyber/deep_forensics", {
467
515
  catalogLane: "text",
468
516
  useOpenRouter: true,
469
517
  });
470
- // → { provider: "openrouter", model: "deepseek/deepseek-v4-pro", ... }
518
+ // → { provider: "openrouter", model: "deepseek/deepseek-v4-pro", routing: "openrouter", ... }
519
+
520
+ // Sync bundled variant (tests, predict paths)
521
+ resolveAndNormalizeBundledInvokeModel("cyber/deep_forensics", {
522
+ catalogLane: "text",
523
+ useOpenRouter: true,
524
+ });
471
525
  ```
472
526
 
473
- Errors: `INVOKE_MODEL_ALIAS_AT_GATEWAY` (unresolved `profile/choice`), `INVOKE_MODEL_SHAPE_INVALID` (unparseable composite or missing provider).
527
+ **Error codes** (on `AIProfilesError.code`):
528
+
529
+ | Code | When |
530
+ |------|------|
531
+ | `INVOKE_MODEL_ALIAS_AT_GATEWAY` | `profile/choice` reached `normalizeInvokeModel` without prior `resolveAIProfile` |
532
+ | `INVOKE_MODEL_SHAPE_INVALID` | Empty model, missing provider, or unparseable composite after normalization |
533
+
534
+ `allowAlias: true` exists for rare internal paths; **gateway and HTTP callers should not use it** — resolve aliases first.
474
535
 
475
536
  ## Sync validation (bundled registry)
476
537
 
@@ -531,14 +592,18 @@ This applies to any `profile/choice` input that resolves to a remote profile cho
531
592
 
532
593
  ### Avoid hardcoded OpenRouter slugs (especially dated previews)
533
594
 
534
- Do **not** copy OpenRouter slugs into app config (`preActionModel`, env vars, etc.) and do **not** use `canonicalSlug` from the catalog for API calls. OpenRouter retires dated preview ids (e.g. `google/gemini-2.5-flash-lite-preview-06-17`) while stable ids remain (`google/gemini-2.5-flash-lite`).
595
+ Do **not** copy OpenRouter slugs into graph JSON or app config (`preActionModel`, `skillModel`, `postActionModel`, env vars, etc.) and do **not** use `canonicalSlug` from the catalog for API calls. OpenRouter retires dated preview ids (e.g. `google/gemini-2.5-flash-lite-preview-06-17`) while stable ids remain (`google/gemini-2.5-flash-lite`).
535
596
 
536
597
  ```ts
537
- // Good — profile pointer + resolver
598
+ // Good — store profile/choice; resolve at execute time
599
+ preActionModel: "cheap/default";
600
+ skillModel: "balanced/default";
601
+ postActionModel: "cheap/default";
602
+
538
603
  const pre = await resolveAIProfile("cheap/default", { catalogLane: "text" });
539
- preActionModel: pre.modelId; // "google/gemini-2.5-flash-lite"
604
+ // pre.modelId → "google/gemini-2.5-flash-lite" (concrete id only after resolution)
540
605
 
541
- // Bad — stale transport slug from an old catalog page or doc
606
+ // Bad — concrete slug in stored config (stale when catalog moves)
542
607
  preActionModel: "google/gemini-2.5-flash-lite-preview-06-17";
543
608
  ```
544
609
 
@@ -649,29 +714,26 @@ The `version` field in the JSON files (and `registryVersion` on resolved output)
649
714
  ## Integration example
650
715
 
651
716
  ```ts
652
- const resolved = await resolveAIProfile(
653
- node.taskConfiguration.aiProfile ?? "balanced/default",
654
- { catalogLane: "text" },
655
- );
717
+ import { resolveAIProfile, resolveAndNormalizeInvokeModel } from "@x12i/ai-profiles";
656
718
 
657
- const modelConfig = {
658
- provider: resolved.provider,
659
- model: resolved.modelId,
660
- routing: resolved.routing,
661
- instructionTier: resolved.instructionTier,
662
- backend: resolved.backend,
663
- temperature: resolved.runtime.temperature,
664
- outputMode: resolved.runtime.outputMode,
665
- toolPolicy: resolved.runtime.toolPolicy,
666
- reasoningEffort: resolved.runtime.reasoningEffort,
719
+ // Graph JSON stores aliases only (merged job defaults + node overrides per task)
720
+ const slots = {
721
+ preActionModel: node.taskConfiguration.modelConfig?.preActionModel ?? "deep/default",
722
+ skillModel: node.taskConfiguration.modelConfig?.skillModel ?? "balanced/default",
723
+ postActionModel: node.taskConfiguration.modelConfig?.postActionModel ?? "cheap/default",
667
724
  };
668
725
 
669
- // Default OpenRouter routing:
670
- // provider: "openrouter", model: "openai/gpt-5.4", routing: "openrouter"
726
+ // Resolve each phase independently at execute time
727
+ const pre = await resolveAIProfile(slots.preActionModel, { catalogLane: "text" });
728
+ const main = await resolveAIProfile(slots.skillModel, { catalogLane: "text" });
729
+ const post = await resolveAIProfile(slots.postActionModel, { catalogLane: "text" });
671
730
 
672
- // Vendor-direct:
673
- // await resolveAIProfile(..., { useOpenRouter: false })
674
- // provider: "openai", model: "gpt-5.4", routing: "direct"
731
+ // Gateway handoff: resolve + normalize invoke tuple
732
+ const mainInvoke = await resolveAndNormalizeInvokeModel(slots.skillModel, {
733
+ catalogLane: "text",
734
+ useOpenRouter: true,
735
+ });
736
+ // mainInvoke → { provider: "openrouter", model: "openai/gpt-5.4", routing: "openrouter", ... }
675
737
  ```
676
738
 
677
739
  ## Profile matrix (FuncX contract)
@@ -690,7 +752,7 @@ Default choice per profile (vendor-native ids in registry; OpenRouter slugs when
690
752
  | `research` | Remote | **`reasoning`** | `default` | `claude-sonnet-4.6` |
691
753
  | `cyber` | Remote | **`reasoning`** | `default` | `claude-sonnet-4.6` |
692
754
  | `content` | Remote | **`default`** | `default` | `claude-sonnet-4.6` |
693
- | `summarization` | Remote | **`default`** | `default` | `claude-haiku-4.5` |
755
+ | `sum` | Remote | **`default`** | `default` | `claude-haiku-4.5` |
694
756
  | `vision` | Remote | **`default`** | `default` | `gpt-5.4` |
695
757
  | `local` | CPU (no OpenRouter) | **`cpu`** | `llama_cpp_gguf` | `funcx:llama-cpp:gguf` |
696
758
  | `local` | CPU | **`cpu`** | `transformersjs_default` | `Xenova/distilbart-cnn-6-6` |
@@ -858,7 +920,13 @@ Workflow YAML is in [`docs/github-workflows/`](docs/github-workflows/). Copy int
858
920
  Consumers install with:
859
921
 
860
922
  ```bash
861
- npm install @x12i/ai-profiles
923
+ npm install @x12i/ai-profiles@^3.2.0
924
+ ```
925
+
926
+ Peer dependency for downstream repos (graphs-studio, graph-engine, ai-tasks):
927
+
928
+ ```json
929
+ "@x12i/ai-profiles": "^3.2.0"
862
930
  ```
863
931
 
864
932
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-profiles",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Resolve AI profile names into concrete model choices with bundled defaults and remote JSON refresh",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",