pi-smart-router 0.1.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.
- package/.pi/extensions/smart-router/command-formatters.ts +167 -0
- package/.pi/extensions/smart-router/commands.ts +242 -0
- package/.pi/extensions/smart-router/dataset-export.ts +142 -0
- package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
- package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
- package/.pi/extensions/smart-router/extension-setup.ts +115 -0
- package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
- package/.pi/extensions/smart-router/index.ts +115 -0
- package/.pi/extensions/smart-router/package.json +3 -0
- package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
- package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
- package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
- package/.pi/extensions/smart-router/routing-context.ts +171 -0
- package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
- package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
- package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
- package/.pi/extensions/smart-router/types.ts +90 -0
- package/.pi/extensions/smart-router/utils.ts +58 -0
- package/README.md +593 -0
- package/bin/pi-smart-router.mjs +79 -0
- package/config/.gitkeep +0 -0
- package/config/hydra-projection-weights.json.example +1169 -0
- package/config/models.yaml.example +58 -0
- package/config/p-success-weights.json.example +19 -0
- package/config/routing-calibration.json.example +1605 -0
- package/config/routing-centroids.json.example +1570 -0
- package/config/routing-clusters.yaml.example +41 -0
- package/data/contrib/example.json +25 -0
- package/dist/api/explain/router-explain.d.ts +47 -0
- package/dist/api/explain/router-explain.d.ts.map +1 -0
- package/dist/api/explain/router-explain.js +101 -0
- package/dist/api/explain/router-explain.js.map +1 -0
- package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
- package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
- package/dist/api/middleware/pi-router-middleware.js +83 -0
- package/dist/api/middleware/pi-router-middleware.js.map +1 -0
- package/dist/cli/smart-router-cli.d.ts +85 -0
- package/dist/cli/smart-router-cli.d.ts.map +1 -0
- package/dist/cli/smart-router-cli.js +293 -0
- package/dist/cli/smart-router-cli.js.map +1 -0
- package/dist/config/defaults.d.ts +7 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +33 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/models-loader.d.ts +22 -0
- package/dist/config/models-loader.d.ts.map +1 -0
- package/dist/config/models-loader.js +51 -0
- package/dist/config/models-loader.js.map +1 -0
- package/dist/config/pi-model-mapper.d.ts +40 -0
- package/dist/config/pi-model-mapper.d.ts.map +1 -0
- package/dist/config/pi-model-mapper.js +216 -0
- package/dist/config/pi-model-mapper.js.map +1 -0
- package/dist/config/routing-clusters-loader.d.ts +35 -0
- package/dist/config/routing-clusters-loader.d.ts.map +1 -0
- package/dist/config/routing-clusters-loader.js +100 -0
- package/dist/config/routing-clusters-loader.js.map +1 -0
- package/dist/domain/delegation/delegation-context.d.ts +23 -0
- package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
- package/dist/domain/delegation/delegation-context.js +73 -0
- package/dist/domain/delegation/delegation-context.js.map +1 -0
- package/dist/domain/delegation/execution-ledger.d.ts +19 -0
- package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
- package/dist/domain/delegation/execution-ledger.js +23 -0
- package/dist/domain/delegation/execution-ledger.js.map +1 -0
- package/dist/domain/delegation/output-headroom.d.ts +39 -0
- package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
- package/dist/domain/delegation/output-headroom.js +78 -0
- package/dist/domain/delegation/output-headroom.js.map +1 -0
- package/dist/domain/matching/cluster-matcher.d.ts +69 -0
- package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
- package/dist/domain/matching/cluster-matcher.js +294 -0
- package/dist/domain/matching/cluster-matcher.js.map +1 -0
- package/dist/domain/matching/embedding-provider.d.ts +21 -0
- package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
- package/dist/domain/matching/embedding-provider.js +42 -0
- package/dist/domain/matching/embedding-provider.js.map +1 -0
- package/dist/domain/matching/hydra-input.d.ts +18 -0
- package/dist/domain/matching/hydra-input.d.ts.map +1 -0
- package/dist/domain/matching/hydra-input.js +40 -0
- package/dist/domain/matching/hydra-input.js.map +1 -0
- package/dist/domain/matching/hydra-matcher.d.ts +103 -0
- package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
- package/dist/domain/matching/hydra-matcher.js +275 -0
- package/dist/domain/matching/hydra-matcher.js.map +1 -0
- package/dist/domain/pinning/cache-economics.d.ts +53 -0
- package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
- package/dist/domain/pinning/cache-economics.js +72 -0
- package/dist/domain/pinning/cache-economics.js.map +1 -0
- package/dist/domain/pinning/loop-escalation.d.ts +41 -0
- package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
- package/dist/domain/pinning/loop-escalation.js +137 -0
- package/dist/domain/pinning/loop-escalation.js.map +1 -0
- package/dist/domain/pinning/session-pinner.d.ts +81 -0
- package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
- package/dist/domain/pinning/session-pinner.js +237 -0
- package/dist/domain/pinning/session-pinner.js.map +1 -0
- package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
- package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
- package/dist/domain/pinning/sub-route-policy.js +72 -0
- package/dist/domain/pinning/sub-route-policy.js.map +1 -0
- package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
- package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
- package/dist/domain/pipeline/router-pipeline.js +807 -0
- package/dist/domain/pipeline/router-pipeline.js.map +1 -0
- package/dist/domain/pipeline/safe-default.d.ts +25 -0
- package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
- package/dist/domain/pipeline/safe-default.js +41 -0
- package/dist/domain/pipeline/safe-default.js.map +1 -0
- package/dist/domain/refactor-guardrails.d.ts +48 -0
- package/dist/domain/refactor-guardrails.d.ts.map +1 -0
- package/dist/domain/refactor-guardrails.js +129 -0
- package/dist/domain/refactor-guardrails.js.map +1 -0
- package/dist/domain/routing/context-fit.d.ts +53 -0
- package/dist/domain/routing/context-fit.d.ts.map +1 -0
- package/dist/domain/routing/context-fit.js +142 -0
- package/dist/domain/routing/context-fit.js.map +1 -0
- package/dist/domain/routing/expected-cost.d.ts +63 -0
- package/dist/domain/routing/expected-cost.d.ts.map +1 -0
- package/dist/domain/routing/expected-cost.js +177 -0
- package/dist/domain/routing/expected-cost.js.map +1 -0
- package/dist/domain/routing/p-success-classifier.d.ts +130 -0
- package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
- package/dist/domain/routing/p-success-classifier.js +350 -0
- package/dist/domain/routing/p-success-classifier.js.map +1 -0
- package/dist/domain/routing/tier-features.d.ts +70 -0
- package/dist/domain/routing/tier-features.d.ts.map +1 -0
- package/dist/domain/routing/tier-features.js +174 -0
- package/dist/domain/routing/tier-features.js.map +1 -0
- package/dist/domain/routing/tool-history-guard.d.ts +32 -0
- package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
- package/dist/domain/routing/tool-history-guard.js +110 -0
- package/dist/domain/routing/tool-history-guard.js.map +1 -0
- package/dist/domain/scoring/multi-objective.d.ts +48 -0
- package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
- package/dist/domain/scoring/multi-objective.js +116 -0
- package/dist/domain/scoring/multi-objective.js.map +1 -0
- package/dist/domain/triage/triage-engine.d.ts +43 -0
- package/dist/domain/triage/triage-engine.d.ts.map +1 -0
- package/dist/domain/triage/triage-engine.js +317 -0
- package/dist/domain/triage/triage-engine.js.map +1 -0
- package/dist/domain/triage/turn-envelope.d.ts +17 -0
- package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
- package/dist/domain/triage/turn-envelope.js +81 -0
- package/dist/domain/triage/turn-envelope.js.map +1 -0
- package/dist/domain/types/entities.d.ts +303 -0
- package/dist/domain/types/entities.d.ts.map +1 -0
- package/dist/domain/types/entities.js +6 -0
- package/dist/domain/types/entities.js.map +1 -0
- package/dist/domain/types/index.d.ts +4 -0
- package/dist/domain/types/index.d.ts.map +1 -0
- package/dist/domain/types/index.js +2 -0
- package/dist/domain/types/index.js.map +1 -0
- package/dist/domain/types/schemas.d.ts +346 -0
- package/dist/domain/types/schemas.d.ts.map +1 -0
- package/dist/domain/types/schemas.js +241 -0
- package/dist/domain/types/schemas.js.map +1 -0
- package/dist/domain/types/store-port.d.ts +44 -0
- package/dist/domain/types/store-port.d.ts.map +1 -0
- package/dist/domain/types/store-port.js +6 -0
- package/dist/domain/types/store-port.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/gemini-provider.d.ts +28 -0
- package/dist/infra/gemini-provider.d.ts.map +1 -0
- package/dist/infra/gemini-provider.js +112 -0
- package/dist/infra/gemini-provider.js.map +1 -0
- package/dist/infra/telemetry.d.ts +51 -0
- package/dist/infra/telemetry.d.ts.map +1 -0
- package/dist/infra/telemetry.js +100 -0
- package/dist/infra/telemetry.js.map +1 -0
- package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
- package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
- package/dist/infrastructure/delegation/provider-error.js +179 -0
- package/dist/infrastructure/delegation/provider-error.js.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
- package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.js +199 -0
- package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.js +92 -0
- package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
- package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
- package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/memory-store.js +87 -0
- package/dist/infrastructure/persistence/memory-store.js.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.js +658 -0
- package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
- package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
- package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
- package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
- package/dist/infrastructure/pricing/price-broker.js +133 -0
- package/dist/infrastructure/pricing/price-broker.js.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
- package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
- package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
- package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
- package/package.json +110 -0
- package/skills/router-backlog-orchestrator/SKILL.md +183 -0
- package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
- package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
- package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
- package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
- package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
- package/src/api/explain/.gitkeep +0 -0
- package/src/api/explain/router-explain.ts +146 -0
- package/src/api/middleware/.gitkeep +0 -0
- package/src/api/middleware/pi-router-middleware.ts +169 -0
- package/src/cli/smart-router-cli.ts +436 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/defaults.ts +35 -0
- package/src/config/models-loader.ts +68 -0
- package/src/config/pi-model-mapper.ts +285 -0
- package/src/config/routing-clusters-loader.ts +148 -0
- package/src/domain/delegation/delegation-context.ts +111 -0
- package/src/domain/delegation/execution-ledger.ts +34 -0
- package/src/domain/delegation/output-headroom.ts +131 -0
- package/src/domain/matching/.gitkeep +0 -0
- package/src/domain/matching/cluster-matcher.ts +474 -0
- package/src/domain/matching/embedding-provider.ts +79 -0
- package/src/domain/matching/hydra-input.ts +53 -0
- package/src/domain/matching/hydra-matcher.ts +454 -0
- package/src/domain/pinning/.gitkeep +0 -0
- package/src/domain/pinning/cache-economics.ts +128 -0
- package/src/domain/pinning/loop-escalation.ts +181 -0
- package/src/domain/pinning/session-pinner.ts +388 -0
- package/src/domain/pinning/sub-route-policy.ts +119 -0
- package/src/domain/pipeline/.gitkeep +0 -0
- package/src/domain/pipeline/router-pipeline.ts +1139 -0
- package/src/domain/pipeline/safe-default.ts +70 -0
- package/src/domain/refactor-guardrails.ts +202 -0
- package/src/domain/routing/context-fit.ts +221 -0
- package/src/domain/routing/expected-cost.ts +320 -0
- package/src/domain/routing/p-success-classifier.ts +511 -0
- package/src/domain/routing/tier-features.ts +290 -0
- package/src/domain/routing/tool-history-guard.ts +159 -0
- package/src/domain/scoring/.gitkeep +0 -0
- package/src/domain/scoring/multi-objective.ts +175 -0
- package/src/domain/triage/.gitkeep +0 -0
- package/src/domain/triage/triage-engine.ts +373 -0
- package/src/domain/triage/turn-envelope.ts +104 -0
- package/src/domain/types/.gitkeep +0 -0
- package/src/domain/types/entities.ts +384 -0
- package/src/domain/types/index.ts +41 -0
- package/src/domain/types/schemas.ts +289 -0
- package/src/domain/types/store-port.ts +59 -0
- package/src/index.ts +103 -0
- package/src/infra/gemini-provider.ts +142 -0
- package/src/infra/telemetry.ts +169 -0
- package/src/infrastructure/delegation/provider-error.ts +251 -0
- package/src/infrastructure/gateway/.gitkeep +0 -0
- package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
- package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
- package/src/infrastructure/hardware/.gitkeep +0 -0
- package/src/infrastructure/hardware/hardware-probe.ts +276 -0
- package/src/infrastructure/local/.gitkeep +0 -0
- package/src/infrastructure/local/local-zero-tier.ts +151 -0
- package/src/infrastructure/persistence/.gitkeep +0 -0
- package/src/infrastructure/persistence/memory-store.ts +122 -0
- package/src/infrastructure/persistence/sqlite-store.ts +900 -0
- package/src/infrastructure/pricing/.gitkeep +0 -0
- package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
- package/src/infrastructure/pricing/price-broker.ts +183 -0
- package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
- package/src/infrastructure/telemetry/.gitkeep +0 -0
- package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
- package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
- package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
- package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
- package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
- package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas mirroring JSON-schema contracts and data-model entities.
|
|
3
|
+
*
|
|
4
|
+
* Canonical contract sources:
|
|
5
|
+
* - specs/001-build-smart-router/contracts/routing-request.schema.json
|
|
6
|
+
* - specs/001-build-smart-router/contracts/routing-decision.schema.json
|
|
7
|
+
* - specs/001-build-smart-router/data-model.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
// ─── Enum schemas ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const TurnTypeSchema = z.enum([
|
|
15
|
+
'planning',
|
|
16
|
+
'tool_result',
|
|
17
|
+
'subagent',
|
|
18
|
+
'main_loop',
|
|
19
|
+
'unknown',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const PinReasonSchema = z.enum([
|
|
23
|
+
'initial',
|
|
24
|
+
'user_forced',
|
|
25
|
+
'loop_escalation',
|
|
26
|
+
'compaction',
|
|
27
|
+
'cache_economics',
|
|
28
|
+
'context_overflow',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const TierSchema = z.enum([
|
|
32
|
+
'zero-tier',
|
|
33
|
+
'economical-cloud',
|
|
34
|
+
'frontier-cloud',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const RoutingStageSchema = z.enum([
|
|
38
|
+
'triage',
|
|
39
|
+
'turn_envelope',
|
|
40
|
+
'session_pin',
|
|
41
|
+
'local_zero',
|
|
42
|
+
'hydra_match',
|
|
43
|
+
'fallback',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export const PriceSourceSchema = z.enum([
|
|
47
|
+
'override',
|
|
48
|
+
'registry',
|
|
49
|
+
'yaml_fallback',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export const MessageRoleSchema = z.enum([
|
|
53
|
+
'user',
|
|
54
|
+
'assistant',
|
|
55
|
+
'system',
|
|
56
|
+
'tool',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// ─── Message (contract: routing-request.schema.json) ─────────────────────────
|
|
60
|
+
|
|
61
|
+
export const MessageSchema = z.object({
|
|
62
|
+
role: MessageRoleSchema,
|
|
63
|
+
content: z.string(),
|
|
64
|
+
tool_call_id: z.string().optional(),
|
|
65
|
+
tool_calls: z.array(z.unknown()).optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── RoutingRequest (contract: routing-request.schema.json) ──────────────────
|
|
69
|
+
|
|
70
|
+
export const RoutingRequestSchema = z.object({
|
|
71
|
+
request_id: z.string().uuid(),
|
|
72
|
+
session_id: z.string().min(1),
|
|
73
|
+
prompt_text: z.string(),
|
|
74
|
+
messages: z.array(MessageSchema).optional(),
|
|
75
|
+
turn_type: TurnTypeSchema.optional(),
|
|
76
|
+
compaction_flag: z.boolean().optional(),
|
|
77
|
+
force_model_id: z.string().optional(),
|
|
78
|
+
candidate_model_id: z.string().optional(),
|
|
79
|
+
estimated_input_tokens: z.number().int().nonnegative().optional(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── SessionPin (data-model.md) ──────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export const SessionPinSchema = z.object({
|
|
85
|
+
session_id: z.string(),
|
|
86
|
+
pinned_model_id: z.string(),
|
|
87
|
+
pin_reason: PinReasonSchema,
|
|
88
|
+
has_ever_switched: z.boolean(),
|
|
89
|
+
consecutive_upstream_errors: z.number().int().nonnegative(),
|
|
90
|
+
consecutive_tool_failures: z.number().int().nonnegative(),
|
|
91
|
+
last_tool_failure_signature: z.string().nullable(),
|
|
92
|
+
created_at: z.string().datetime(),
|
|
93
|
+
updated_at: z.string().datetime(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── ModelProfile (data-model.md) ────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export const ModelCapabilitiesSchema = z.object({
|
|
99
|
+
reasoning: z.number().min(0).max(1),
|
|
100
|
+
code_gen: z.number().min(0).max(1),
|
|
101
|
+
tool_use: z.number().min(0).max(1),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const ModelPerformanceSchema = z.object({
|
|
105
|
+
latency_p50_ms: z.number().optional(),
|
|
106
|
+
verbosity_factor: z.number().optional(),
|
|
107
|
+
cache_friendly: z.boolean().optional(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const ModelPricingSchema = z.object({
|
|
111
|
+
registry_key: z.string().optional(),
|
|
112
|
+
fallback_cost_per_1m: z.number(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const ModelLimitsSchema = z.object({
|
|
116
|
+
max_input_tokens: z.number().int().positive().optional(),
|
|
117
|
+
max_output_tokens: z.number().int().positive().optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const ModelProfileSchema = z.object({
|
|
121
|
+
id: z.string(),
|
|
122
|
+
tier: TierSchema,
|
|
123
|
+
provider: z.string(),
|
|
124
|
+
endpoint: z.string().optional(),
|
|
125
|
+
capabilities: ModelCapabilitiesSchema,
|
|
126
|
+
performance: ModelPerformanceSchema.optional(),
|
|
127
|
+
pricing: ModelPricingSchema,
|
|
128
|
+
limits: ModelLimitsSchema.optional(),
|
|
129
|
+
healthy: z.boolean().optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** Zod-validated model profile — aligned with {@link ModelProfile} in entities.ts. */
|
|
133
|
+
export type ValidatedModelProfile = z.infer<typeof ModelProfileSchema>;
|
|
134
|
+
|
|
135
|
+
// ─── CandidateScore (contract: routing-decision.schema.json) ─────────────────
|
|
136
|
+
|
|
137
|
+
export const CandidateScoreSchema = z.object({
|
|
138
|
+
model_id: z.string(),
|
|
139
|
+
score: z.number(),
|
|
140
|
+
shortfall: z.number(),
|
|
141
|
+
rejected_reason: z.string().nullable(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ─── RoutingDecision (contract: routing-decision.schema.json) ────────────────
|
|
145
|
+
|
|
146
|
+
export const RoutingDecisionSchema = z.object({
|
|
147
|
+
request_id: z.string().uuid(),
|
|
148
|
+
selected_model_id: z.string(),
|
|
149
|
+
tier: TierSchema,
|
|
150
|
+
stage: RoutingStageSchema,
|
|
151
|
+
reason_code: z.string(),
|
|
152
|
+
candidates: z.array(CandidateScoreSchema).optional(),
|
|
153
|
+
estimated_cost_usd: z.number().nonnegative().optional(),
|
|
154
|
+
routing_latency_ms: z.number().nonnegative(),
|
|
155
|
+
pin_reason: PinReasonSchema.nullable(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ─── PriceCatalog (data-model.md) ────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export const PriceCatalogSchema = z.object({
|
|
161
|
+
registry_snapshot: z.record(z.string(), z.number()),
|
|
162
|
+
registry_limits_snapshot: z.record(z.string(), ModelLimitsSchema).optional(),
|
|
163
|
+
user_overrides: z.record(z.string(), z.number()),
|
|
164
|
+
last_updated: z.string().datetime(),
|
|
165
|
+
source: PriceSourceSchema,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── RoutingTelemetry (data-model.md) ────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
export const RoutingTelemetrySchema = z.object({
|
|
171
|
+
timestamp: z.string().datetime(),
|
|
172
|
+
session_id: z.string(),
|
|
173
|
+
request_id: z.string(),
|
|
174
|
+
turn_type: z.string(),
|
|
175
|
+
stage: z.string(),
|
|
176
|
+
reason_code: z.string(),
|
|
177
|
+
selected_model_id: z.string(),
|
|
178
|
+
estimated_cost_usd: z.number(),
|
|
179
|
+
routing_latency_ms: z.number(),
|
|
180
|
+
pin_reason: z.string().nullable(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ─── Operator configuration schema (data-model.md § Configuration) ───────────
|
|
184
|
+
|
|
185
|
+
export const FrugalityConfigSchema = z.object({
|
|
186
|
+
lambda_cost: z.number().min(0).max(1),
|
|
187
|
+
lambda_latency: z.number().min(0),
|
|
188
|
+
lambda_verbosity: z.number().min(0),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export const LoopEscalationConfigSchema = z.object({
|
|
192
|
+
threshold: z.number().int().positive(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const PricingConfigSchema = z.object({
|
|
196
|
+
staleness_days: z.number().int().positive(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const LocalConfigSchema = z.object({
|
|
200
|
+
min_memory_gb_full: z.number().positive(),
|
|
201
|
+
min_memory_gb_classification: z.number().positive(),
|
|
202
|
+
battery_threshold_pct: z.number().min(0).max(100),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export const HydraConfigSchema = z.object({
|
|
206
|
+
artifact_cache_path: z.string(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export const LowIntensityWeightsSchema = z.object({
|
|
210
|
+
prompt_shortness: z.number().min(0),
|
|
211
|
+
token_shortness: z.number().min(0),
|
|
212
|
+
cyclomatic_low: z.number().min(0),
|
|
213
|
+
trivial_signal: z.number().min(0),
|
|
214
|
+
complex_inverse: z.number().min(0),
|
|
215
|
+
triage_verdict: z.number().min(0),
|
|
216
|
+
turn_type: z.number().min(0),
|
|
217
|
+
no_tool_context: z.number().min(0),
|
|
218
|
+
message_shallow: z.number().min(0),
|
|
219
|
+
prose_ratio: z.number().min(0),
|
|
220
|
+
requirement_low: z.number().min(0),
|
|
221
|
+
cluster_signal: z.number().min(0),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export const LowIntensityConfigSchema = z.object({
|
|
225
|
+
weights: LowIntensityWeightsSchema,
|
|
226
|
+
high_threshold: z.number().min(0).max(1),
|
|
227
|
+
low_threshold: z.number().min(0).max(1),
|
|
228
|
+
/** Minimum P_success_cheap to bias toward economical/local tier (SP-105). */
|
|
229
|
+
p_success_alpha: z.number().min(0).max(1),
|
|
230
|
+
}).superRefine((value, ctx) => {
|
|
231
|
+
if (value.high_threshold <= value.low_threshold) {
|
|
232
|
+
ctx.addIssue({
|
|
233
|
+
code: z.ZodIssueCode.custom,
|
|
234
|
+
message: 'high_threshold must be greater than low_threshold',
|
|
235
|
+
path: ['high_threshold'],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export const RoutingClustersConfigSchema = z.object({
|
|
241
|
+
config_path: z.string().min(1),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/** Stable snake_case cluster id — used as reason-code suffix (`cluster_${id}`). */
|
|
245
|
+
export const RoutingClusterIdSchema = z
|
|
246
|
+
.string()
|
|
247
|
+
.min(1)
|
|
248
|
+
.regex(/^[a-z][a-z0-9_]*$/, 'Cluster id must be lowercase snake_case');
|
|
249
|
+
|
|
250
|
+
export const RoutingClusterSchema = z.object({
|
|
251
|
+
id: RoutingClusterIdSchema,
|
|
252
|
+
tier_bias: TierSchema,
|
|
253
|
+
reference_prompts: z.array(z.string().min(1)).min(1),
|
|
254
|
+
min_similarity: z.number().min(0).max(1),
|
|
255
|
+
min_margin: z.number().min(0).max(1),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export const RoutingClustersFileSchema = z
|
|
259
|
+
.object({
|
|
260
|
+
clusters: z.array(RoutingClusterSchema).min(1),
|
|
261
|
+
})
|
|
262
|
+
.superRefine((value, ctx) => {
|
|
263
|
+
const seen = new Set<string>();
|
|
264
|
+
for (const [index, cluster] of value.clusters.entries()) {
|
|
265
|
+
if (seen.has(cluster.id)) {
|
|
266
|
+
ctx.addIssue({
|
|
267
|
+
code: z.ZodIssueCode.custom,
|
|
268
|
+
message: `Duplicate cluster id: ${cluster.id}`,
|
|
269
|
+
path: ['clusters', index, 'id'],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
seen.add(cluster.id);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
export const OperatorConfigSchema = z.object({
|
|
277
|
+
frugality: FrugalityConfigSchema,
|
|
278
|
+
loop_escalation: LoopEscalationConfigSchema,
|
|
279
|
+
pricing: PricingConfigSchema,
|
|
280
|
+
local: LocalConfigSchema,
|
|
281
|
+
hydra: HydraConfigSchema,
|
|
282
|
+
low_intensity: LowIntensityConfigSchema,
|
|
283
|
+
routing_clusters: RoutingClustersConfigSchema.optional(),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── Inferred types ──────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export type OperatorConfig = z.infer<typeof OperatorConfigSchema>;
|
|
289
|
+
export type LowIntensityConfig = z.infer<typeof LowIntensityConfigSchema>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence port for routing state.
|
|
3
|
+
* Implementations live in infrastructure/ (SQLite, in-memory for tests).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ModelProfile, PriceCatalog, RoutingDatasetRecord, RoutingOutcomeRecord, RoutingTelemetry, SessionPin } from './entities.js';
|
|
7
|
+
|
|
8
|
+
export interface ListTelemetryOptions {
|
|
9
|
+
readonly limit?: number;
|
|
10
|
+
readonly sessionId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ListDatasetOptions {
|
|
14
|
+
readonly limit?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListOutcomeOptions {
|
|
18
|
+
readonly limit?: number;
|
|
19
|
+
readonly requestId?: string;
|
|
20
|
+
readonly sessionId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StorePort {
|
|
24
|
+
/** Retrieve an active session pin, or null if unpinned. */
|
|
25
|
+
getSessionPin(sessionId: string): Promise<SessionPin | null>;
|
|
26
|
+
|
|
27
|
+
/** Upsert a session pin (create or replace). */
|
|
28
|
+
putSessionPin(pin: SessionPin): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/** Delete a session pin (e.g. on explicit unpin). */
|
|
31
|
+
deleteSessionPin(sessionId: string): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/** Load the full model fleet catalog. */
|
|
34
|
+
getModelProfiles(): Promise<readonly ModelProfile[]>;
|
|
35
|
+
|
|
36
|
+
/** Load the current price catalog. */
|
|
37
|
+
getPriceCatalog(): Promise<PriceCatalog | null>;
|
|
38
|
+
|
|
39
|
+
/** Persist an updated price catalog. */
|
|
40
|
+
putPriceCatalog(catalog: PriceCatalog): Promise<void>;
|
|
41
|
+
|
|
42
|
+
/** Append a routing telemetry audit record (sync hot path). */
|
|
43
|
+
appendTelemetry(entry: RoutingTelemetry): void;
|
|
44
|
+
|
|
45
|
+
/** List recent telemetry rows, newest first. */
|
|
46
|
+
listTelemetry(options?: ListTelemetryOptions): Promise<readonly RoutingTelemetry[]>;
|
|
47
|
+
|
|
48
|
+
/** Append a privacy-safe routing dataset record (sync hot path). */
|
|
49
|
+
appendDatasetRecord(entry: RoutingDatasetRecord): void;
|
|
50
|
+
|
|
51
|
+
/** List recent dataset rows, newest first. */
|
|
52
|
+
listDatasetRecords(options?: ListDatasetOptions): Promise<readonly RoutingDatasetRecord[]>;
|
|
53
|
+
|
|
54
|
+
/** Append a behavioral outcome label (sync hot path). */
|
|
55
|
+
appendOutcomeRecord(entry: RoutingOutcomeRecord): void;
|
|
56
|
+
|
|
57
|
+
/** List recent outcome rows, newest first. */
|
|
58
|
+
listOutcomeRecords(options?: ListOutcomeOptions): Promise<readonly RoutingOutcomeRecord[]>;
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public package exports — pi-smart-router.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the router factory (T022) and pi lifecycle hook registrar (T021).
|
|
5
|
+
* Full pi integration uses `.pi/extensions/smart-router/`; library embedders
|
|
6
|
+
* route via `dispatch.dispatch()` after calling `register(hooks)`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ModelProfile, RoutingDecision } from './domain/types/index.js';
|
|
10
|
+
import { loadModels, type FleetCatalog } from './config/models-loader.js';
|
|
11
|
+
import {
|
|
12
|
+
loadRoutingClusters,
|
|
13
|
+
clusterReasonCode,
|
|
14
|
+
type TextEmbedder,
|
|
15
|
+
} from './config/routing-clusters-loader.js';
|
|
16
|
+
import {
|
|
17
|
+
GatewayDispatch,
|
|
18
|
+
type GatewayDispatchOptions,
|
|
19
|
+
} from './infrastructure/gateway/gateway-dispatch.js';
|
|
20
|
+
import {
|
|
21
|
+
createPiRouterMiddleware,
|
|
22
|
+
LifecycleHookState,
|
|
23
|
+
type PiRouterMiddleware,
|
|
24
|
+
type PiExtensionHooks,
|
|
25
|
+
} from './api/middleware/pi-router-middleware.js';
|
|
26
|
+
|
|
27
|
+
/** Package identifier for diagnostics and telemetry. */
|
|
28
|
+
export const PACKAGE_NAME = 'pi-smart-router' as const;
|
|
29
|
+
|
|
30
|
+
// ─── Router factory types ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface RouterFactoryOptions {
|
|
33
|
+
readonly modelsPath?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RouterHandle {
|
|
37
|
+
readonly version: string;
|
|
38
|
+
readonly middleware: PiRouterMiddleware;
|
|
39
|
+
readonly dispatch: GatewayDispatch;
|
|
40
|
+
readonly fleet: readonly ModelProfile[];
|
|
41
|
+
readonly register: (hooks: PiExtensionHooks) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Router factory (T022) ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function createRouter(options?: RouterFactoryOptions): RouterHandle {
|
|
47
|
+
const catalog = loadModels(
|
|
48
|
+
options?.modelsPath ? { filePath: options.modelsPath } : undefined,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return createRouterFromCatalog(catalog);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createRouterFromCatalog(catalog: FleetCatalog): RouterHandle {
|
|
55
|
+
return createRouterFromFleet([...catalog.models]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CreateRouterFromFleetOptions extends GatewayDispatchOptions {
|
|
59
|
+
readonly lifecycleHookState?: LifecycleHookState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createRouterFromFleet(
|
|
63
|
+
fleet: ModelProfile[],
|
|
64
|
+
options?: CreateRouterFromFleetOptions,
|
|
65
|
+
): RouterHandle {
|
|
66
|
+
const { lifecycleHookState, ...dispatchOptions } = options ?? {};
|
|
67
|
+
const dispatch = new GatewayDispatch(fleet, dispatchOptions);
|
|
68
|
+
const middleware = createPiRouterMiddleware(
|
|
69
|
+
lifecycleHookState !== undefined ? { lifecycleHookState } : undefined,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
version: 'pi-smart-router',
|
|
74
|
+
middleware,
|
|
75
|
+
dispatch,
|
|
76
|
+
fleet,
|
|
77
|
+
register: middleware.register,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Re-exports for consumer convenience ─────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export type { RoutingDecision, ModelProfile };
|
|
84
|
+
export {
|
|
85
|
+
loadRoutingClusters,
|
|
86
|
+
clusterReasonCode,
|
|
87
|
+
};
|
|
88
|
+
export type { TextEmbedder };
|
|
89
|
+
export type { RoutingClusterCatalog } from './domain/types/index.js';
|
|
90
|
+
export type {
|
|
91
|
+
PiRouterMiddleware,
|
|
92
|
+
PiRouterMiddlewareOptions,
|
|
93
|
+
PiExtensionHooks,
|
|
94
|
+
PiExtensionContext,
|
|
95
|
+
PiProviderRequestEvent,
|
|
96
|
+
PiContextEvent,
|
|
97
|
+
PiModelSelectEvent,
|
|
98
|
+
PiSessionManager,
|
|
99
|
+
LifecycleFlags,
|
|
100
|
+
} from './api/middleware/pi-router-middleware.js';
|
|
101
|
+
export { createPiRouterMiddleware, LifecycleHookState } from './api/middleware/pi-router-middleware.js';
|
|
102
|
+
export type { GatewayDispatchOptions } from './infrastructure/gateway/gateway-dispatch.js';
|
|
103
|
+
export type { PipelineOptions } from './domain/pipeline/router-pipeline.js';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini provider error classification and failover policy.
|
|
3
|
+
*
|
|
4
|
+
* thought_signature 400s are client/protocol validation errors (FR-018),
|
|
5
|
+
* not infrastructure outages — they must return terminal errors, not trigger
|
|
6
|
+
* stream delegation failover.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AssistantMessage } from '@earendil-works/pi-ai/compat';
|
|
10
|
+
|
|
11
|
+
export interface GeminiProviderError {
|
|
12
|
+
readonly statusCode?: number;
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
readonly message?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gemini returns 400 INVALID_ARGUMENT when tool-call replay omits thought_signature.
|
|
19
|
+
*/
|
|
20
|
+
export function isGeminiThoughtSignatureError(parsed: GeminiProviderError): boolean {
|
|
21
|
+
return (
|
|
22
|
+
parsed.statusCode === 400 &&
|
|
23
|
+
parsed.message?.includes('thought_signature') === true
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isGeminiThoughtSignatureAssistantError(message: AssistantMessage): boolean {
|
|
28
|
+
if (message.stopReason !== 'error' || !message.errorMessage) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return isGeminiThoughtSignatureError(parseGeminiErrorBlob(message.errorMessage));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when a Gemini provider error should trigger failover.
|
|
36
|
+
* thought_signature validation failures are terminal — never failover.
|
|
37
|
+
*/
|
|
38
|
+
export function shouldFailoverOnGeminiError(parsed: GeminiProviderError): boolean {
|
|
39
|
+
if (isGeminiThoughtSignatureError(parsed)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return parsed.statusCode !== undefined && (parsed.statusCode >= 500 || parsed.statusCode === 429);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const GEMINI_THOUGHT_SIGNATURE_DOCS =
|
|
46
|
+
'https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures';
|
|
47
|
+
const PI_THOUGHT_SIGNATURE_ISSUE = 'https://github.com/earendil-works/pi/issues/6342';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format a Gemini thought_signature validation error with operator workarounds.
|
|
51
|
+
*/
|
|
52
|
+
export function formatGeminiThoughtSignatureErrorMessage(errorMessage: string): string {
|
|
53
|
+
const parsed = parseGeminiErrorBlob(errorMessage);
|
|
54
|
+
const prefixParts: string[] = [];
|
|
55
|
+
if (parsed.statusCode !== undefined) {
|
|
56
|
+
prefixParts.push(String(parsed.statusCode));
|
|
57
|
+
}
|
|
58
|
+
if (parsed.code) {
|
|
59
|
+
prefixParts.push(parsed.code);
|
|
60
|
+
}
|
|
61
|
+
const summary =
|
|
62
|
+
parsed.message && prefixParts.length > 0
|
|
63
|
+
? `${prefixParts.join(' ')}: ${parsed.message}`
|
|
64
|
+
: parsed.message ?? 'Provider error (thought_signature validation)';
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
summary,
|
|
68
|
+
'',
|
|
69
|
+
'Gemini rejected this request because a prior tool call is missing its thought_signature (protocol validation, not provider outage).',
|
|
70
|
+
'Workarounds: start a fresh session with /new, or route to a non-Google model until pi preserves thought signatures in replay.',
|
|
71
|
+
`Docs: ${GEMINI_THOUGHT_SIGNATURE_DOCS}`,
|
|
72
|
+
`Upstream: ${PI_THOUGHT_SIGNATURE_ISSUE}`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseGeminiErrorBlob(errorMessage: string): GeminiProviderError {
|
|
77
|
+
const trimmed = errorMessage.trim();
|
|
78
|
+
if (!trimmed.startsWith('{')) {
|
|
79
|
+
return trimmed.includes('thought_signature') ? { statusCode: 400, message: trimmed } : {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(trimmed) as {
|
|
84
|
+
error?: { code?: unknown; status?: unknown; message?: unknown };
|
|
85
|
+
code?: unknown;
|
|
86
|
+
status?: unknown;
|
|
87
|
+
message?: unknown;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const nested = parsed.error;
|
|
91
|
+
const statusCode =
|
|
92
|
+
parseNumericCode(nested?.code) ??
|
|
93
|
+
parseNumericCode(nested?.status) ??
|
|
94
|
+
parseNumericCode(parsed.code) ??
|
|
95
|
+
parseNumericCode(parsed.status);
|
|
96
|
+
|
|
97
|
+
const code =
|
|
98
|
+
typeof nested?.status === 'string'
|
|
99
|
+
? nested.status
|
|
100
|
+
: typeof parsed.status === 'string'
|
|
101
|
+
? parsed.status
|
|
102
|
+
: undefined;
|
|
103
|
+
|
|
104
|
+
let message =
|
|
105
|
+
typeof nested?.message === 'string'
|
|
106
|
+
? nested.message
|
|
107
|
+
: typeof parsed.message === 'string'
|
|
108
|
+
? parsed.message
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
if (message?.trim().startsWith('{')) {
|
|
112
|
+
try {
|
|
113
|
+
const inner = JSON.parse(message) as { error?: { message?: unknown }; message?: unknown };
|
|
114
|
+
if (typeof inner.error?.message === 'string') {
|
|
115
|
+
message = inner.error.message;
|
|
116
|
+
} else if (typeof inner.message === 'string') {
|
|
117
|
+
message = inner.message;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// keep outer message
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...(statusCode !== undefined ? { statusCode } : {}),
|
|
126
|
+
...(code ? { code } : {}),
|
|
127
|
+
...(message ? { message } : {}),
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseNumericCode(value: unknown): number | undefined {
|
|
135
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
139
|
+
return Number.parseInt(value, 10);
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|