ai-runtime-engine 1.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/CHANGELOG.md +78 -0
- package/LICENSE +15 -0
- package/README.md +489 -0
- package/dist/artifacts/artifacts.d.ts +28 -0
- package/dist/artifacts/artifacts.js +46 -0
- package/dist/benchmark/benchmark.d.ts +23 -0
- package/dist/benchmark/benchmark.js +40 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.js +161 -0
- package/dist/cli/commands/cleanup.d.ts +47 -0
- package/dist/cli/commands/cleanup.js +96 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/doctor.d.ts +57 -0
- package/dist/cli/commands/doctor.js +86 -0
- package/dist/cli/commands/executions.d.ts +9 -0
- package/dist/cli/commands/executions.js +25 -0
- package/dist/cli/commands/info.d.ts +43 -0
- package/dist/cli/commands/info.js +53 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +75 -0
- package/dist/cli/commands/inspect.d.ts +16 -0
- package/dist/cli/commands/inspect.js +60 -0
- package/dist/cli/commands/phase2.d.ts +22 -0
- package/dist/cli/commands/phase2.js +83 -0
- package/dist/cli/commands/route.d.ts +14 -0
- package/dist/cli/commands/route.js +49 -0
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +37 -0
- package/dist/cli/commands/setup.d.ts +34 -0
- package/dist/cli/commands/setup.js +104 -0
- package/dist/cli/commands/skills.d.ts +28 -0
- package/dist/cli/commands/skills.js +48 -0
- package/dist/cli/commands/test.d.ts +7 -0
- package/dist/cli/commands/test.js +29 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +16 -0
- package/dist/cli/interactive/repl.d.ts +6 -0
- package/dist/cli/interactive/repl.js +45 -0
- package/dist/cli/interactive/session.d.ts +36 -0
- package/dist/cli/interactive/session.js +356 -0
- package/dist/cli/prompt.d.ts +6 -0
- package/dist/cli/prompt.js +18 -0
- package/dist/cli/render.d.ts +7 -0
- package/dist/cli/render.js +14 -0
- package/dist/comparison/analysis.d.ts +46 -0
- package/dist/comparison/analysis.js +177 -0
- package/dist/comparison/comparator.d.ts +46 -0
- package/dist/comparison/comparator.js +270 -0
- package/dist/comparison/comparison.d.ts +140 -0
- package/dist/comparison/comparison.js +9 -0
- package/dist/comparison/render.d.ts +7 -0
- package/dist/comparison/render.js +66 -0
- package/dist/config/defaults.d.ts +52 -0
- package/dist/config/defaults.js +56 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +50 -0
- package/dist/config/providerDefaults.d.ts +17 -0
- package/dist/config/providerDefaults.js +61 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/schema.js +78 -0
- package/dist/context/budget.d.ts +13 -0
- package/dist/context/budget.js +17 -0
- package/dist/context/compiler.d.ts +61 -0
- package/dist/context/compiler.js +125 -0
- package/dist/context/tokens.d.ts +19 -0
- package/dist/context/tokens.js +38 -0
- package/dist/conversations/conversations.d.ts +38 -0
- package/dist/conversations/conversations.js +64 -0
- package/dist/core/capabilities/evidence.d.ts +40 -0
- package/dist/core/capabilities/evidence.js +102 -0
- package/dist/core/capabilities/overlay.d.ts +15 -0
- package/dist/core/capabilities/overlay.js +0 -0
- package/dist/core/capabilities/taxonomy.d.ts +19 -0
- package/dist/core/capabilities/taxonomy.js +25 -0
- package/dist/core/fallback/errors.d.ts +30 -0
- package/dist/core/fallback/errors.js +80 -0
- package/dist/core/fallback/fallback.d.ts +40 -0
- package/dist/core/fallback/fallback.js +82 -0
- package/dist/core/fallback/retryPolicy.d.ts +11 -0
- package/dist/core/fallback/retryPolicy.js +14 -0
- package/dist/core/health/health.d.ts +3 -0
- package/dist/core/health/health.js +5 -0
- package/dist/core/health/monitor.d.ts +23 -0
- package/dist/core/health/monitor.js +82 -0
- package/dist/core/policies/budget.d.ts +19 -0
- package/dist/core/policies/budget.js +37 -0
- package/dist/core/registry/builtinTasks.d.ts +8 -0
- package/dist/core/registry/builtinTasks.js +54 -0
- package/dist/core/registry/registry.d.ts +18 -0
- package/dist/core/registry/registry.js +33 -0
- package/dist/core/registry/taskRegistry.d.ts +15 -0
- package/dist/core/registry/taskRegistry.js +30 -0
- package/dist/core/router/confidence.d.ts +7 -0
- package/dist/core/router/confidence.js +20 -0
- package/dist/core/router/dimensions.d.ts +16 -0
- package/dist/core/router/dimensions.js +60 -0
- package/dist/core/router/executor.d.ts +16 -0
- package/dist/core/router/executor.js +25 -0
- package/dist/core/router/filter.d.ts +34 -0
- package/dist/core/router/filter.js +113 -0
- package/dist/core/router/normalize.d.ts +30 -0
- package/dist/core/router/normalize.js +119 -0
- package/dist/core/router/request.d.ts +4 -0
- package/dist/core/router/request.js +21 -0
- package/dist/core/router/router.d.ts +32 -0
- package/dist/core/router/router.js +195 -0
- package/dist/core/router/routingPrefs.d.ts +11 -0
- package/dist/core/router/routingPrefs.js +30 -0
- package/dist/core/router/scorer.d.ts +19 -0
- package/dist/core/router/scorer.js +50 -0
- package/dist/core/router/weights.d.ts +9 -0
- package/dist/core/router/weights.js +31 -0
- package/dist/core/validation/validator.d.ts +16 -0
- package/dist/core/validation/validator.js +33 -0
- package/dist/discovery/modelCatalog.d.ts +28 -0
- package/dist/discovery/modelCatalog.js +105 -0
- package/dist/discovery/openapi.d.ts +25 -0
- package/dist/discovery/openapi.js +76 -0
- package/dist/executions/checkpoint.d.ts +26 -0
- package/dist/executions/checkpoint.js +114 -0
- package/dist/executions/execution.d.ts +51 -0
- package/dist/executions/execution.js +8 -0
- package/dist/executions/store.d.ts +52 -0
- package/dist/executions/store.js +124 -0
- package/dist/generation/generateAdapter.d.ts +17 -0
- package/dist/generation/generateAdapter.js +30 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +107 -0
- package/dist/learning/feedback.d.ts +9 -0
- package/dist/learning/feedback.js +18 -0
- package/dist/learning/learningStore.d.ts +68 -0
- package/dist/learning/learningStore.js +138 -0
- package/dist/learning/performanceStore.d.ts +27 -0
- package/dist/learning/performanceStore.js +0 -0
- package/dist/marketplace/presets.d.ts +24 -0
- package/dist/marketplace/presets.js +52 -0
- package/dist/mcp/mcp.d.ts +31 -0
- package/dist/mcp/mcp.js +54 -0
- package/dist/memory/bm25.d.ts +16 -0
- package/dist/memory/bm25.js +56 -0
- package/dist/memory/classifier.d.ts +14 -0
- package/dist/memory/classifier.js +17 -0
- package/dist/memory/memory.d.ts +80 -0
- package/dist/memory/memory.js +191 -0
- package/dist/orchestration/executor.d.ts +35 -0
- package/dist/orchestration/executor.js +65 -0
- package/dist/orchestration/orchestrator.d.ts +42 -0
- package/dist/orchestration/orchestrator.js +63 -0
- package/dist/orchestration/plan.d.ts +37 -0
- package/dist/orchestration/plan.js +70 -0
- package/dist/orchestration/planner.d.ts +29 -0
- package/dist/orchestration/planner.js +69 -0
- package/dist/plugin/ai.d.ts +82 -0
- package/dist/plugin/ai.js +167 -0
- package/dist/probing/probe.d.ts +25 -0
- package/dist/probing/probe.js +63 -0
- package/dist/providers/factory.d.ts +18 -0
- package/dist/providers/factory.js +54 -0
- package/dist/providers/httpClient.d.ts +34 -0
- package/dist/providers/httpClient.js +80 -0
- package/dist/providers/httpProvider.d.ts +49 -0
- package/dist/providers/httpProvider.js +135 -0
- package/dist/providers/mock/demo.d.ts +13 -0
- package/dist/providers/mock/demo.js +58 -0
- package/dist/providers/mock/mockProvider.d.ts +35 -0
- package/dist/providers/mock/mockProvider.js +121 -0
- package/dist/providers/mock/scenarios.d.ts +44 -0
- package/dist/providers/mock/scenarios.js +30 -0
- package/dist/providers/provider.d.ts +26 -0
- package/dist/providers/provider.js +11 -0
- package/dist/providers/wire/anthropicWire.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +83 -0
- package/dist/providers/wire/openaiWire.d.ts +7 -0
- package/dist/providers/wire/openaiWire.js +81 -0
- package/dist/providers/wire/registry.d.ts +8 -0
- package/dist/providers/wire/registry.js +20 -0
- package/dist/providers/wire/types.d.ts +39 -0
- package/dist/providers/wire/types.js +24 -0
- package/dist/runtime/config.d.ts +31 -0
- package/dist/runtime/config.js +121 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +11 -0
- package/dist/runtime/events.d.ts +99 -0
- package/dist/runtime/events.js +82 -0
- package/dist/runtime/host.d.ts +27 -0
- package/dist/runtime/host.js +7 -0
- package/dist/runtime/intent/classifier.d.ts +30 -0
- package/dist/runtime/intent/classifier.js +60 -0
- package/dist/runtime/intent/signals.d.ts +19 -0
- package/dist/runtime/intent/signals.js +46 -0
- package/dist/runtime/modes/availability.d.ts +11 -0
- package/dist/runtime/modes/availability.js +17 -0
- package/dist/runtime/modes/chat.d.ts +18 -0
- package/dist/runtime/modes/chat.js +67 -0
- package/dist/runtime/modes/modeResolver.d.ts +43 -0
- package/dist/runtime/modes/modeResolver.js +78 -0
- package/dist/runtime/policy.d.ts +72 -0
- package/dist/runtime/policy.js +59 -0
- package/dist/runtime/providerView.d.ts +62 -0
- package/dist/runtime/providerView.js +105 -0
- package/dist/runtime/routing.d.ts +26 -0
- package/dist/runtime/routing.js +65 -0
- package/dist/runtime/runtime.d.ts +191 -0
- package/dist/runtime/runtime.js +718 -0
- package/dist/runtime/types.d.ts +153 -0
- package/dist/runtime/types.js +9 -0
- package/dist/runtime/workspace/detectors.d.ts +15 -0
- package/dist/runtime/workspace/detectors.js +57 -0
- package/dist/runtime/workspace/workspace.d.ts +29 -0
- package/dist/runtime/workspace/workspace.js +116 -0
- package/dist/security/credentials.d.ts +26 -0
- package/dist/security/credentials.js +34 -0
- package/dist/security/redact.d.ts +16 -0
- package/dist/security/redact.js +57 -0
- package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
- package/dist/skills/builtins/fileAnalyzer.js +47 -0
- package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
- package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
- package/dist/skills/discovery.d.ts +61 -0
- package/dist/skills/discovery.js +211 -0
- package/dist/skills/manifest.d.ts +30 -0
- package/dist/skills/manifest.js +75 -0
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.js +22 -0
- package/dist/skills/skill.d.ts +64 -0
- package/dist/skills/skill.js +8 -0
- package/dist/store/area.d.ts +54 -0
- package/dist/store/area.js +164 -0
- package/dist/store/paths.d.ts +15 -0
- package/dist/store/paths.js +48 -0
- package/dist/store/store.d.ts +59 -0
- package/dist/store/store.js +140 -0
- package/dist/telemetry/sinks/file.d.ts +12 -0
- package/dist/telemetry/sinks/file.js +28 -0
- package/dist/telemetry/telemetry.d.ts +36 -0
- package/dist/telemetry/telemetry.js +63 -0
- package/dist/tools/builtins/filesystem.d.ts +7 -0
- package/dist/tools/builtins/filesystem.js +53 -0
- package/dist/tools/builtins/git.d.ts +10 -0
- package/dist/tools/builtins/git.js +66 -0
- package/dist/tools/builtins/shell.d.ts +17 -0
- package/dist/tools/builtins/shell.js +91 -0
- package/dist/tools/jail.d.ts +12 -0
- package/dist/tools/jail.js +98 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +24 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +20 -0
- package/dist/tools/runner.d.ts +23 -0
- package/dist/tools/runner.js +64 -0
- package/dist/tools/tool.d.ts +53 -0
- package/dist/tools/tool.js +24 -0
- package/dist/tools/untrusted.d.ts +13 -0
- package/dist/tools/untrusted.js +30 -0
- package/dist/types.d.ts +460 -0
- package/dist/types.js +12 -0
- package/dist/util/clock.d.ts +6 -0
- package/dist/util/clock.js +4 -0
- package/dist/util/extractJson.d.ts +8 -0
- package/dist/util/extractJson.js +54 -0
- package/dist/verification/verify.d.ts +26 -0
- package/dist/verification/verify.js +67 -0
- package/docs/GUIDE.md +358 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +78 -0
- package/docs/router.md +376 -0
- package/docs/security.md +55 -0
- package/package.json +67 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single HTTP execution path: per-request `AbortSignal.timeout`, same-provider retry on
|
|
3
|
+
* 429/5xx/network with `Retry-After`, and the `response_format` graceful-degrade for gateways that
|
|
4
|
+
* reject it. Two deliberate design choices:
|
|
5
|
+
* - `fetch` is INJECTABLE (so every adapter test runs offline against a mocked fetch);
|
|
6
|
+
* - transport is SPLIT from classification — this layer throws a redacted, categorized AIError; the
|
|
7
|
+
* cross-provider fallback decides what to do next.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: the request URL is never interpolated into an error (undici embeds it, and it can carry a
|
|
10
|
+
* key-in-query); error bodies are redacted and truncated; the API key lives only in the header the
|
|
11
|
+
* caller built, never in anything this function stores or throws.
|
|
12
|
+
*/
|
|
13
|
+
import { AIError, isRetryable, statusToCategory } from '../core/fallback/errors.js';
|
|
14
|
+
import { redactString } from '../security/redact.js';
|
|
15
|
+
import { systemClock } from '../util/clock.js';
|
|
16
|
+
export async function callHttp(input) {
|
|
17
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
18
|
+
const clock = input.clock ?? systemClock;
|
|
19
|
+
const { providerId, model } = input;
|
|
20
|
+
let jsonMode = input.jsonMode;
|
|
21
|
+
let lastError;
|
|
22
|
+
for (let attempt = 1; attempt <= input.maxRetries + 1; attempt += 1) {
|
|
23
|
+
const req = input.build(jsonMode);
|
|
24
|
+
const started = clock.now();
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetchImpl(req.url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: req.headers,
|
|
29
|
+
body: JSON.stringify(req.body),
|
|
30
|
+
signal: AbortSignal.timeout(input.timeoutMs),
|
|
31
|
+
});
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
// response_format degrade: a gateway that 400s naming it doesn't support JSON mode.
|
|
35
|
+
if (res.status === 400 && jsonMode && /response_format/i.test(text)) {
|
|
36
|
+
jsonMode = false;
|
|
37
|
+
input.onJsonModeDisabled?.();
|
|
38
|
+
lastError = new AIError('gateway rejected response_format — retrying without JSON mode', { category: 'INVALID_REQUEST', status: 400, retryable: true, providerId, model });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const excerpt = redactString(text.slice(0, 300));
|
|
42
|
+
const category = statusToCategory(res.status);
|
|
43
|
+
const retryable = isRetryable(category);
|
|
44
|
+
const err = new AIError(`${providerId} returned HTTP ${res.status}: ${excerpt}`, { category, status: res.status, retryable, providerId, model });
|
|
45
|
+
if (!retryable)
|
|
46
|
+
throw err;
|
|
47
|
+
lastError = err;
|
|
48
|
+
const retryAfter = Number(res.headers.get('retry-after'));
|
|
49
|
+
await clock.sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1000, 30_000) : attempt * 2000);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let json;
|
|
53
|
+
try {
|
|
54
|
+
json = JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
58
|
+
throw new AIError(`${providerId} returned a non-JSON body (${why})`, { category: 'RESPONSE_VALIDATION', status: res.status, retryable: false, providerId, model });
|
|
59
|
+
}
|
|
60
|
+
return { status: res.status, json, durationMs: clock.now() - started, attempts: attempt, jsonModeUsed: jsonMode };
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e instanceof AIError && !e.retryable)
|
|
64
|
+
throw e;
|
|
65
|
+
if (e instanceof AIError) {
|
|
66
|
+
lastError = e;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// fetch/undici embeds the full URL in its message — use only the error NAME, never the message.
|
|
70
|
+
const name = e instanceof Error ? e.name : 'Error';
|
|
71
|
+
const isTimeout = name === 'TimeoutError' || name === 'AbortError';
|
|
72
|
+
lastError = new AIError(`${providerId} request failed (${name})`, { category: isTimeout ? 'TIMEOUT' : 'NETWORK', retryable: true, providerId, model });
|
|
73
|
+
}
|
|
74
|
+
if (attempt > input.maxRetries)
|
|
75
|
+
break;
|
|
76
|
+
await clock.sleep(attempt * 2000);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError ?? new AIError(`${providerId} request failed`, { category: 'NETWORK', retryable: true, providerId, model });
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpProvider — the single concrete adapter behind the whole provider fleet. Every real provider is
|
|
3
|
+
* this class configured with a wire shape, a base URL, a credential, and a resolved model list. That
|
|
4
|
+
* is why "adding a provider" is a preset/config change, not new routing code. It covers both the
|
|
5
|
+
* OpenAI-compatible fleet (Gemini/Groq/Ollama/OpenRouter/Cerebras/Mistral/custom) and Anthropic-native
|
|
6
|
+
* (via the anthropic wire) through one execute() path.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY: the key is read from the Credential only at header-build time; discover() reports the base
|
|
9
|
+
* URL ORIGIN only; nothing here returns or logs the key.
|
|
10
|
+
*/
|
|
11
|
+
import type { AIProvider } from './provider.js';
|
|
12
|
+
import type { AIRequest, AIResponse, CapabilityProfile, ExecutionEstimate, HealthStatus, ModelMetadata, PrivacyClass, ProviderKind, ProviderMetadata } from '../types.js';
|
|
13
|
+
import type { WireShape } from './wire/types.js';
|
|
14
|
+
import type { FetchLike } from './httpClient.js';
|
|
15
|
+
import type { Clock } from '../util/clock.js';
|
|
16
|
+
import type { Credential } from '../security/credentials.js';
|
|
17
|
+
export interface HttpProviderConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
kind: ProviderKind;
|
|
21
|
+
wireShape: WireShape;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
credential: Credential;
|
|
24
|
+
privacyClass: PrivacyClass;
|
|
25
|
+
models: ModelMetadata[];
|
|
26
|
+
defaultModel?: string;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
supportsModelListing?: boolean;
|
|
29
|
+
requiresKey?: boolean;
|
|
30
|
+
fetchImpl?: FetchLike;
|
|
31
|
+
clock?: Clock;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class HttpProvider implements AIProvider {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly kind: ProviderKind;
|
|
39
|
+
readonly privacyClass: PrivacyClass;
|
|
40
|
+
private readonly cfg;
|
|
41
|
+
private jsonMode;
|
|
42
|
+
constructor(config: HttpProviderConfig);
|
|
43
|
+
discover(): Promise<ProviderMetadata>;
|
|
44
|
+
healthCheck(): Promise<HealthStatus>;
|
|
45
|
+
listModels(): Promise<ModelMetadata[]>;
|
|
46
|
+
getCapabilities(model: string): Promise<CapabilityProfile>;
|
|
47
|
+
estimate(request: AIRequest): Promise<ExecutionEstimate>;
|
|
48
|
+
execute(request: AIRequest): Promise<AIResponse>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpProvider — the single concrete adapter behind the whole provider fleet. Every real provider is
|
|
3
|
+
* this class configured with a wire shape, a base URL, a credential, and a resolved model list. That
|
|
4
|
+
* is why "adding a provider" is a preset/config change, not new routing code. It covers both the
|
|
5
|
+
* OpenAI-compatible fleet (Gemini/Groq/Ollama/OpenRouter/Cerebras/Mistral/custom) and Anthropic-native
|
|
6
|
+
* (via the anthropic wire) through one execute() path.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY: the key is read from the Credential only at header-build time; discover() reports the base
|
|
9
|
+
* URL ORIGIN only; nothing here returns or logs the key.
|
|
10
|
+
*/
|
|
11
|
+
import { AIError } from '../core/fallback/errors.js';
|
|
12
|
+
import { emptyProfile } from '../core/capabilities/evidence.js';
|
|
13
|
+
import { extractJson } from '../util/extractJson.js';
|
|
14
|
+
import { getWire } from './wire/registry.js';
|
|
15
|
+
import { callHttp } from './httpClient.js';
|
|
16
|
+
function originOf(url) {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(url).origin;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class HttpProvider {
|
|
25
|
+
id;
|
|
26
|
+
name;
|
|
27
|
+
kind;
|
|
28
|
+
privacyClass;
|
|
29
|
+
cfg;
|
|
30
|
+
jsonMode = true;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.cfg = config;
|
|
33
|
+
this.id = config.id;
|
|
34
|
+
this.name = config.name ?? config.id;
|
|
35
|
+
this.kind = config.kind;
|
|
36
|
+
this.privacyClass = config.privacyClass;
|
|
37
|
+
}
|
|
38
|
+
async discover() {
|
|
39
|
+
return {
|
|
40
|
+
id: this.id,
|
|
41
|
+
name: this.name,
|
|
42
|
+
kind: this.kind,
|
|
43
|
+
baseOrigin: originOf(this.cfg.baseUrl),
|
|
44
|
+
authMode: this.cfg.requiresKey === false ? 'none' : 'env',
|
|
45
|
+
supportsModelListing: this.cfg.supportsModelListing ?? false,
|
|
46
|
+
privacyClass: this.privacyClass,
|
|
47
|
+
models: this.cfg.models,
|
|
48
|
+
...(this.cfg.defaultModel ? { defaultModel: this.cfg.defaultModel } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async healthCheck() {
|
|
52
|
+
const needsKey = this.cfg.requiresKey !== false;
|
|
53
|
+
if (needsKey && !this.cfg.credential.hasKey) {
|
|
54
|
+
return { providerId: this.id, state: 'NOT_CONFIGURED', routable: false, checkedAt: 0, detail: `missing ${this.cfg.credential.envName ?? 'API key'}` };
|
|
55
|
+
}
|
|
56
|
+
return { providerId: this.id, state: 'AVAILABLE', routable: true, checkedAt: 0 };
|
|
57
|
+
}
|
|
58
|
+
async listModels() {
|
|
59
|
+
return this.cfg.models;
|
|
60
|
+
}
|
|
61
|
+
async getCapabilities(model) {
|
|
62
|
+
return this.cfg.models.find((m) => m.id === model)?.capabilities ?? emptyProfile();
|
|
63
|
+
}
|
|
64
|
+
async estimate(request) {
|
|
65
|
+
const model = this.cfg.models.find((m) => m.id === request.model);
|
|
66
|
+
const inTokens = Math.ceil((request.input.text?.length ?? 0) / 4);
|
|
67
|
+
const estimate = {
|
|
68
|
+
providerId: this.id,
|
|
69
|
+
model: request.model,
|
|
70
|
+
estInputTokens: inTokens,
|
|
71
|
+
contextFits: model?.contextWindow ? inTokens < model.contextWindow : true,
|
|
72
|
+
};
|
|
73
|
+
if (model?.pricing) {
|
|
74
|
+
estimate.estCost = { amount: ((model.pricing.inputPerMTok ?? 0) * inTokens) / 1_000_000, currency: model.pricing.currency ?? 'USD', evidence: 'inferred' };
|
|
75
|
+
}
|
|
76
|
+
if (model?.latency?.p50Ms !== undefined)
|
|
77
|
+
estimate.estLatencyMs = { p50: model.latency.p50Ms, evidence: model.latency.evidence };
|
|
78
|
+
return estimate;
|
|
79
|
+
}
|
|
80
|
+
async execute(request) {
|
|
81
|
+
const needsKey = this.cfg.requiresKey !== false;
|
|
82
|
+
const apiKey = this.cfg.credential.use();
|
|
83
|
+
if (needsKey && !apiKey) {
|
|
84
|
+
throw new AIError(`no ${this.cfg.credential.envName ?? 'API key'} in the environment`, { category: 'AUTHENTICATION', retryable: false, providerId: this.id, model: request.model });
|
|
85
|
+
}
|
|
86
|
+
const wire = getWire(this.cfg.wireShape);
|
|
87
|
+
const maxTokens = request.params?.maxTokens ?? 2000;
|
|
88
|
+
const ctx = {
|
|
89
|
+
baseUrl: this.cfg.baseUrl,
|
|
90
|
+
model: request.model,
|
|
91
|
+
maxTokens,
|
|
92
|
+
...(apiKey ? { apiKey } : {}),
|
|
93
|
+
...(this.cfg.headers ? { headers: this.cfg.headers } : {}),
|
|
94
|
+
};
|
|
95
|
+
const result = await callHttp({
|
|
96
|
+
build: (jsonMode) => wire.buildRequest(request, ctx, jsonMode),
|
|
97
|
+
providerId: this.id,
|
|
98
|
+
model: request.model,
|
|
99
|
+
timeoutMs: request.timeoutMs || this.cfg.timeoutMs || 90_000,
|
|
100
|
+
maxRetries: this.cfg.maxRetries ?? 2,
|
|
101
|
+
jsonMode: this.jsonMode,
|
|
102
|
+
...(this.cfg.fetchImpl ? { fetchImpl: this.cfg.fetchImpl } : {}),
|
|
103
|
+
...(this.cfg.clock ? { clock: this.cfg.clock } : {}),
|
|
104
|
+
onJsonModeDisabled: () => {
|
|
105
|
+
this.jsonMode = false;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const parsed = wire.readResponse(result.json);
|
|
109
|
+
const response = {
|
|
110
|
+
finishReason: parsed.finishReason,
|
|
111
|
+
providerId: this.id,
|
|
112
|
+
model: request.model,
|
|
113
|
+
latencyMs: result.durationMs,
|
|
114
|
+
};
|
|
115
|
+
if (parsed.text !== undefined)
|
|
116
|
+
response.text = parsed.text;
|
|
117
|
+
if (parsed.toolCalls !== undefined)
|
|
118
|
+
response.toolCalls = parsed.toolCalls;
|
|
119
|
+
const wantsJson = request.output?.format === 'json' || request.output?.format === 'structured_output';
|
|
120
|
+
if (parsed.json !== undefined)
|
|
121
|
+
response.json = parsed.json;
|
|
122
|
+
else if (wantsJson && parsed.text)
|
|
123
|
+
response.json = extractJson(parsed.text);
|
|
124
|
+
if (parsed.usage) {
|
|
125
|
+
const inTok = parsed.usage.inputTokens;
|
|
126
|
+
const outTok = parsed.usage.outputTokens;
|
|
127
|
+
response.usage = {
|
|
128
|
+
...(inTok !== undefined ? { inputTokens: inTok } : {}),
|
|
129
|
+
...(outTok !== undefined ? { outputTokens: outTok } : {}),
|
|
130
|
+
...(inTok !== undefined && outTok !== undefined ? { totalTokens: inTok + outTok } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A demo AI wired with two deterministic mock providers — a frontier "alpha" and a fast/cheap "beta".
|
|
3
|
+
* Used by the CLI and examples so the whole loop is exercisable offline with no keys. Not part of the
|
|
4
|
+
* routing core; purely a convenience for demonstrations and smoke checks.
|
|
5
|
+
*/
|
|
6
|
+
import { AI } from '../../plugin/ai.js';
|
|
7
|
+
import type { MockBehavior } from './scenarios.js';
|
|
8
|
+
import type { AIRequest } from '../../types.js';
|
|
9
|
+
export interface DemoOptions {
|
|
10
|
+
alphaBehavior?: MockBehavior | ((req: AIRequest) => MockBehavior);
|
|
11
|
+
betaBehavior?: MockBehavior | ((req: AIRequest) => MockBehavior);
|
|
12
|
+
}
|
|
13
|
+
export declare function buildDemoAI(opts?: DemoOptions): AI;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A demo AI wired with two deterministic mock providers — a frontier "alpha" and a fast/cheap "beta".
|
|
3
|
+
* Used by the CLI and examples so the whole loop is exercisable offline with no keys. Not part of the
|
|
4
|
+
* routing core; purely a convenience for demonstrations and smoke checks.
|
|
5
|
+
*/
|
|
6
|
+
import { AI } from '../../plugin/ai.js';
|
|
7
|
+
import { MockProvider } from './mockProvider.js';
|
|
8
|
+
import { makeModel } from './scenarios.js';
|
|
9
|
+
export function buildDemoAI(opts = {}) {
|
|
10
|
+
const alpha = new MockProvider({
|
|
11
|
+
id: 'alpha',
|
|
12
|
+
name: 'Alpha (frontier)',
|
|
13
|
+
privacyClass: 'cloud',
|
|
14
|
+
models: [
|
|
15
|
+
makeModel({
|
|
16
|
+
providerId: 'alpha',
|
|
17
|
+
id: 'alpha-large',
|
|
18
|
+
caps: {
|
|
19
|
+
intelligence: ['reasoning', 'code_generation', 'code_analysis', 'summarization', 'extraction', 'classification', 'planning'],
|
|
20
|
+
output: ['text', 'code', 'json', 'structured_output'],
|
|
21
|
+
input: ['text', 'image', 'file'],
|
|
22
|
+
agent: ['tool_calling', 'function_calling'],
|
|
23
|
+
},
|
|
24
|
+
contextWindow: 200_000,
|
|
25
|
+
quality: { tier: 'frontier' },
|
|
26
|
+
pricing: { inputPerMTok: 3, outputPerMTok: 15, currency: 'USD' },
|
|
27
|
+
latencyP50Ms: 1800,
|
|
28
|
+
successRate: 0.98,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
behavior: opts.alphaBehavior ?? { kind: 'ok' },
|
|
32
|
+
latencyMs: 1,
|
|
33
|
+
});
|
|
34
|
+
const beta = new MockProvider({
|
|
35
|
+
id: 'beta',
|
|
36
|
+
name: 'Beta (fast/cheap)',
|
|
37
|
+
privacyClass: 'cloud',
|
|
38
|
+
models: [
|
|
39
|
+
makeModel({
|
|
40
|
+
providerId: 'beta',
|
|
41
|
+
id: 'beta-fast',
|
|
42
|
+
caps: {
|
|
43
|
+
intelligence: ['reasoning', 'code_generation', 'code_analysis'],
|
|
44
|
+
output: ['text', 'code', 'json'],
|
|
45
|
+
input: ['text'],
|
|
46
|
+
},
|
|
47
|
+
contextWindow: 128_000,
|
|
48
|
+
quality: { tier: 'strong' },
|
|
49
|
+
pricing: { inputPerMTok: 0.5, outputPerMTok: 0.8, currency: 'USD' },
|
|
50
|
+
latencyP50Ms: 400,
|
|
51
|
+
successRate: 0.97,
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
behavior: opts.betaBehavior ?? { kind: 'ok' },
|
|
55
|
+
latencyMs: 1,
|
|
56
|
+
});
|
|
57
|
+
return new AI().registerProvider(alpha).registerProvider(beta);
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockProvider — a deterministic, no-network AIProvider. It is the keystone for offline development and the
|
|
3
|
+
* chaos-test backbone: scripted behaviors (ok / timeout / rate_limit / server_error / malformed /
|
|
4
|
+
* auth_fail) let every routing, fallback, and validation path be proven offline with no API keys.
|
|
5
|
+
*/
|
|
6
|
+
import type { AIProvider } from '../provider.js';
|
|
7
|
+
import type { AIRequest, AIResponse, CapabilityProfile, ExecutionEstimate, HealthStatus, ModelMetadata, PrivacyClass, ProviderMetadata } from '../../types.js';
|
|
8
|
+
import type { MockBehavior } from './scenarios.js';
|
|
9
|
+
export interface MockProviderConfig {
|
|
10
|
+
id: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
privacyClass?: PrivacyClass;
|
|
13
|
+
models: ModelMetadata[];
|
|
14
|
+
/** Fixed behavior or a function of the request. Defaults to `ok`. */
|
|
15
|
+
behavior?: MockBehavior | ((req: AIRequest) => MockBehavior);
|
|
16
|
+
latencyMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class MockProvider implements AIProvider {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly kind: "mock";
|
|
22
|
+
readonly privacyClass: PrivacyClass;
|
|
23
|
+
private readonly models;
|
|
24
|
+
private readonly behavior;
|
|
25
|
+
private readonly latencyMs;
|
|
26
|
+
/** Number of times execute() was actually invoked — lets tests assert a poisoned provider is not re-hammered. */
|
|
27
|
+
executeCalls: number;
|
|
28
|
+
constructor(config: MockProviderConfig);
|
|
29
|
+
discover(): Promise<ProviderMetadata>;
|
|
30
|
+
healthCheck(): Promise<HealthStatus>;
|
|
31
|
+
listModels(): Promise<ModelMetadata[]>;
|
|
32
|
+
getCapabilities(model: string): Promise<CapabilityProfile>;
|
|
33
|
+
estimate(request: AIRequest): Promise<ExecutionEstimate>;
|
|
34
|
+
execute(request: AIRequest): Promise<AIResponse>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockProvider — a deterministic, no-network AIProvider. It is the keystone for offline development and the
|
|
3
|
+
* chaos-test backbone: scripted behaviors (ok / timeout / rate_limit / server_error / malformed /
|
|
4
|
+
* auth_fail) let every routing, fallback, and validation path be proven offline with no API keys.
|
|
5
|
+
*/
|
|
6
|
+
import { AIError } from '../../core/fallback/errors.js';
|
|
7
|
+
import { emptyProfile } from '../../core/capabilities/evidence.js';
|
|
8
|
+
export class MockProvider {
|
|
9
|
+
id;
|
|
10
|
+
name;
|
|
11
|
+
kind = 'mock';
|
|
12
|
+
privacyClass;
|
|
13
|
+
models;
|
|
14
|
+
behavior;
|
|
15
|
+
latencyMs;
|
|
16
|
+
/** Number of times execute() was actually invoked — lets tests assert a poisoned provider is not re-hammered. */
|
|
17
|
+
executeCalls = 0;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.id = config.id;
|
|
20
|
+
this.name = config.name ?? config.id;
|
|
21
|
+
this.privacyClass = config.privacyClass ?? 'cloud';
|
|
22
|
+
this.models = config.models;
|
|
23
|
+
this.behavior = config.behavior ?? { kind: 'ok' };
|
|
24
|
+
this.latencyMs = config.latencyMs ?? 1;
|
|
25
|
+
}
|
|
26
|
+
async discover() {
|
|
27
|
+
return {
|
|
28
|
+
id: this.id,
|
|
29
|
+
name: this.name,
|
|
30
|
+
kind: this.kind,
|
|
31
|
+
authMode: 'none',
|
|
32
|
+
supportsModelListing: true,
|
|
33
|
+
privacyClass: this.privacyClass,
|
|
34
|
+
models: this.models,
|
|
35
|
+
...(this.models[0] ? { defaultModel: this.models[0].id } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async healthCheck() {
|
|
39
|
+
return { providerId: this.id, state: 'AVAILABLE', routable: true, checkedAt: 0 };
|
|
40
|
+
}
|
|
41
|
+
async listModels() {
|
|
42
|
+
return this.models;
|
|
43
|
+
}
|
|
44
|
+
async getCapabilities(model) {
|
|
45
|
+
return this.models.find((m) => m.id === model)?.capabilities ?? emptyProfile();
|
|
46
|
+
}
|
|
47
|
+
async estimate(request) {
|
|
48
|
+
const model = this.models.find((m) => m.id === request.model);
|
|
49
|
+
const inTokens = Math.ceil((request.input.text?.length ?? 0) / 4);
|
|
50
|
+
const estimate = {
|
|
51
|
+
providerId: this.id,
|
|
52
|
+
model: request.model,
|
|
53
|
+
estInputTokens: inTokens,
|
|
54
|
+
contextFits: model?.contextWindow ? inTokens < model.contextWindow : true,
|
|
55
|
+
};
|
|
56
|
+
if (model?.pricing) {
|
|
57
|
+
estimate.estCost = {
|
|
58
|
+
amount: ((model.pricing.inputPerMTok ?? 0) * inTokens) / 1_000_000,
|
|
59
|
+
currency: model.pricing.currency ?? 'USD',
|
|
60
|
+
evidence: 'inferred',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (model?.latency?.p50Ms !== undefined) {
|
|
64
|
+
estimate.estLatencyMs = { p50: model.latency.p50Ms, evidence: 'inferred' };
|
|
65
|
+
}
|
|
66
|
+
return estimate;
|
|
67
|
+
}
|
|
68
|
+
async execute(request) {
|
|
69
|
+
this.executeCalls += 1;
|
|
70
|
+
const behavior = typeof this.behavior === 'function' ? this.behavior(request) : this.behavior;
|
|
71
|
+
const base = { providerId: this.id, model: request.model };
|
|
72
|
+
switch (behavior.kind) {
|
|
73
|
+
case 'timeout':
|
|
74
|
+
throw new AIError(`${this.id} timed out`, { category: 'TIMEOUT', ...base });
|
|
75
|
+
case 'rate_limit':
|
|
76
|
+
throw new AIError(`${this.id} rate limited`, { category: 'RATE_LIMIT', status: 429, ...base });
|
|
77
|
+
case 'server_error':
|
|
78
|
+
throw new AIError(`${this.id} returned HTTP 503`, { category: 'PROVIDER', status: 503, ...base });
|
|
79
|
+
case 'auth_fail':
|
|
80
|
+
throw new AIError(`${this.id} authentication failed`, { category: 'AUTHENTICATION', status: 401, ...base });
|
|
81
|
+
case 'malformed':
|
|
82
|
+
throw new AIError(`${this.id} returned an unparseable response`, { category: 'RESPONSE_VALIDATION', ...base });
|
|
83
|
+
case 'ok': {
|
|
84
|
+
// A request that supplies tools gets a tool call back (used by the tool-calling probe).
|
|
85
|
+
if (request.tools && request.tools.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
finishReason: 'tool_calls',
|
|
88
|
+
providerId: this.id,
|
|
89
|
+
model: request.model,
|
|
90
|
+
latencyMs: this.latencyMs,
|
|
91
|
+
toolCalls: [{ name: request.tools[0].name, arguments: {} }],
|
|
92
|
+
usage: { inputTokens: 4, outputTokens: 4 },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const wantsJson = request.output?.format === 'json' || request.output?.format === 'structured_output';
|
|
96
|
+
const response = {
|
|
97
|
+
finishReason: 'stop',
|
|
98
|
+
providerId: this.id,
|
|
99
|
+
model: request.model,
|
|
100
|
+
latencyMs: this.latencyMs,
|
|
101
|
+
usage: { inputTokens: Math.ceil((request.input.text?.length ?? 0) / 4), outputTokens: 8 },
|
|
102
|
+
};
|
|
103
|
+
if (wantsJson)
|
|
104
|
+
response.json = behavior.json ?? { ok: true, task: request.taskId };
|
|
105
|
+
else
|
|
106
|
+
response.text = behavior.text ?? `mock(${this.id}/${request.model}) handled ${request.taskId}`;
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
case 'ok_text': {
|
|
110
|
+
return {
|
|
111
|
+
finishReason: 'stop',
|
|
112
|
+
providerId: this.id,
|
|
113
|
+
model: request.model,
|
|
114
|
+
latencyMs: this.latencyMs,
|
|
115
|
+
text: behavior.text ?? `mock(${this.id}/${request.model}) text-only`,
|
|
116
|
+
usage: { inputTokens: 4, outputTokens: 8 },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers for the MockProvider: build normalized ModelMetadata quickly, and name the failure
|
|
3
|
+
* behaviors the chaos/integration tests script. Shipped in `src/` so downstream consumers can use the
|
|
4
|
+
* same deterministic double in their own tests.
|
|
5
|
+
*/
|
|
6
|
+
import type { BooleanCapabilityGroup } from '../../core/capabilities/taxonomy.js';
|
|
7
|
+
import type { Evidence, ModelMetadata, Pricing, PrivacyClass, QualityTier } from '../../types.js';
|
|
8
|
+
export type MockBehavior = {
|
|
9
|
+
kind: 'ok';
|
|
10
|
+
text?: string;
|
|
11
|
+
json?: unknown;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'ok_text';
|
|
14
|
+
text?: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'timeout';
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'rate_limit';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'server_error';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'malformed';
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'auth_fail';
|
|
25
|
+
};
|
|
26
|
+
export interface MakeModelInput {
|
|
27
|
+
providerId: string;
|
|
28
|
+
id: string;
|
|
29
|
+
caps?: Partial<Record<BooleanCapabilityGroup, string[]>>;
|
|
30
|
+
contextWindow?: number;
|
|
31
|
+
evidence?: Evidence;
|
|
32
|
+
quality?: {
|
|
33
|
+
tier?: QualityTier;
|
|
34
|
+
score?: number;
|
|
35
|
+
evidence?: Evidence;
|
|
36
|
+
};
|
|
37
|
+
pricing?: Pricing;
|
|
38
|
+
latencyP50Ms?: number;
|
|
39
|
+
successRate?: number;
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Build a ModelMetadata with declared capabilities tagged at one evidence level (default `documented`). */
|
|
43
|
+
export declare function makeModel(input: MakeModelInput): ModelMetadata;
|
|
44
|
+
export declare const DEFAULT_PRIVACY: PrivacyClass;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers for the MockProvider: build normalized ModelMetadata quickly, and name the failure
|
|
3
|
+
* behaviors the chaos/integration tests script. Shipped in `src/` so downstream consumers can use the
|
|
4
|
+
* same deterministic double in their own tests.
|
|
5
|
+
*/
|
|
6
|
+
import { profileFromDeclared } from '../../core/capabilities/evidence.js';
|
|
7
|
+
/** Build a ModelMetadata with declared capabilities tagged at one evidence level (default `documented`). */
|
|
8
|
+
export function makeModel(input) {
|
|
9
|
+
const evidence = input.evidence ?? 'documented';
|
|
10
|
+
const capabilities = profileFromDeclared({ ...input.caps, contextWindow: input.contextWindow }, evidence);
|
|
11
|
+
const model = {
|
|
12
|
+
providerId: input.providerId,
|
|
13
|
+
id: input.id,
|
|
14
|
+
capabilities,
|
|
15
|
+
enabled: input.enabled ?? true,
|
|
16
|
+
source: evidence,
|
|
17
|
+
};
|
|
18
|
+
if (input.contextWindow !== undefined)
|
|
19
|
+
model.contextWindow = input.contextWindow;
|
|
20
|
+
if (input.pricing !== undefined)
|
|
21
|
+
model.pricing = input.pricing;
|
|
22
|
+
if (input.quality !== undefined)
|
|
23
|
+
model.quality = { evidence: input.quality.evidence ?? 'documented', ...(input.quality.tier !== undefined ? { tier: input.quality.tier } : {}), ...(input.quality.score !== undefined ? { score: input.quality.score } : {}) };
|
|
24
|
+
if (input.latencyP50Ms !== undefined)
|
|
25
|
+
model.latency = { p50Ms: input.latencyP50Ms, evidence: 'inferred' };
|
|
26
|
+
if (input.successRate !== undefined)
|
|
27
|
+
model.reliability = { successRate: input.successRate, evidence: 'inferred' };
|
|
28
|
+
return model;
|
|
29
|
+
}
|
|
30
|
+
export const DEFAULT_PRIVACY = 'cloud';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider contract — the single seam between the vendor-agnostic core and the outside world.
|
|
3
|
+
*
|
|
4
|
+
* `core/` only ever holds `AIProvider` instances and reads capabilities / privacyClass / health /
|
|
5
|
+
* estimates THROUGH this interface. Adding a provider means implementing this (usually by configuring
|
|
6
|
+
* the generic adapter); it never means editing `core/`.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY: an implementation must hold its API key non-enumerably and must never place a key,
|
|
9
|
+
* a full request URL, or a key-in-query anywhere in the values it returns from these methods.
|
|
10
|
+
*/
|
|
11
|
+
import type { AIRequest, AIResponse, CapabilityProfile, ExecutionEstimate, HealthStatus, ModelMetadata, PrivacyClass, ProviderKind, ProviderMetadata } from '../types.js';
|
|
12
|
+
export interface AIProvider {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly kind: ProviderKind;
|
|
16
|
+
/** `local` may receive high-sensitivity input; `cloud` is the fail-closed default. First-class so core reads it via the contract. */
|
|
17
|
+
readonly privacyClass: PrivacyClass;
|
|
18
|
+
/** One-shot startup discovery: identity + models + capabilities the provider can report. */
|
|
19
|
+
discover(): Promise<ProviderMetadata>;
|
|
20
|
+
healthCheck(): Promise<HealthStatus>;
|
|
21
|
+
listModels(): Promise<ModelMetadata[]>;
|
|
22
|
+
/** Resolve capabilities for one model. Providers own capability resolution; core never reads the catalog directly. */
|
|
23
|
+
getCapabilities(model: string): Promise<CapabilityProfile>;
|
|
24
|
+
execute(request: AIRequest): Promise<AIResponse>;
|
|
25
|
+
estimate(request: AIRequest): Promise<ExecutionEstimate>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider contract — the single seam between the vendor-agnostic core and the outside world.
|
|
3
|
+
*
|
|
4
|
+
* `core/` only ever holds `AIProvider` instances and reads capabilities / privacyClass / health /
|
|
5
|
+
* estimates THROUGH this interface. Adding a provider means implementing this (usually by configuring
|
|
6
|
+
* the generic adapter); it never means editing `core/`.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY: an implementation must hold its API key non-enumerably and must never place a key,
|
|
9
|
+
* a full request URL, or a key-in-query anywhere in the values it returns from these methods.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic-native wire (`POST {baseUrl}/v1/messages`). A distinct shape from OpenAI-compat:
|
|
3
|
+
* `x-api-key` + `anthropic-version` headers, top-level `system`, `content[]`/`stop_reason` response.
|
|
4
|
+
*/
|
|
5
|
+
import type { WireModule } from './types.js';
|
|
6
|
+
export declare const anthropicWire: WireModule;
|