@wardby/cli 0.3.0 → 0.4.1
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/.env.example +22 -3
- package/README.md +11 -9
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +68 -10
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/protocol.d.ts +17 -1
- package/dist/coding/protocol.js +17 -6
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/config/providers.d.ts +25 -0
- package/dist/config/providers.js +71 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +17 -10
- package/dist/core/budget-groups.js +15 -12
- package/dist/core/coding-queue.d.ts +3 -0
- package/dist/core/coding-queue.js +6 -2
- package/dist/core/coding-service-status.d.ts +11 -0
- package/dist/core/coding-service-status.js +17 -0
- package/dist/core/cost-report.d.ts +88 -0
- package/dist/core/cost-report.js +248 -0
- package/dist/core/dispatch.d.ts +42 -2
- package/dist/core/dispatch.js +144 -26
- package/dist/core/engine-native.js +17 -4
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/host-events.d.ts +24 -1
- package/dist/core/host-events.js +341 -1
- package/dist/core/host-status.d.ts +19 -2
- package/dist/core/host-status.js +47 -30
- package/dist/core/issue-bridge.d.ts +60 -0
- package/dist/core/issue-bridge.js +189 -0
- package/dist/core/issue-dedupe.d.ts +70 -0
- package/dist/core/issue-dedupe.js +255 -0
- package/dist/core/issue-events.d.ts +42 -0
- package/dist/core/issue-events.js +155 -0
- package/dist/core/issue-status.d.ts +29 -0
- package/dist/core/issue-status.js +241 -0
- package/dist/core/issue-tracker-tools.d.ts +64 -0
- package/dist/core/issue-tracker-tools.js +850 -0
- package/dist/core/model-usage.d.ts +10 -0
- package/dist/core/model-usage.js +24 -0
- package/dist/core/reconciler.d.ts +8 -4
- package/dist/core/reconciler.js +15 -4
- package/dist/core/review-host-tools.js +10 -3
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +5 -2
- package/dist/core/runner.js +178 -27
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/tool-names.js +3 -0
- package/dist/core/webhooks.d.ts +9 -1
- package/dist/core/webhooks.js +19 -1
- package/dist/env.js +6 -1
- package/dist/generated/prisma/browser.d.ts +66 -0
- package/dist/generated/prisma/client.d.ts +66 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
- package/dist/generated/prisma/enums.d.ts +7 -0
- package/dist/generated/prisma/enums.js +6 -0
- package/dist/generated/prisma/internal/class.d.ts +99 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
- package/dist/generated/prisma/models/Agent.d.ts +389 -1
- package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
- package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
- package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
- package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
- package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +933 -1
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.js +1 -0
- package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
- package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
- package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
- package/dist/generated/prisma/models/RunModelUsage.js +1 -0
- package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +9 -0
- package/dist/help-index.json +355 -16
- package/dist/import/neutral-schema.d.ts +16 -16
- package/dist/knowledge/check.d.ts +13 -0
- package/dist/knowledge/check.js +69 -0
- package/dist/knowledge/cli.d.ts +14 -0
- package/dist/knowledge/cli.js +67 -0
- package/dist/knowledge/concept.d.ts +54 -0
- package/dist/knowledge/concept.js +78 -0
- package/dist/knowledge/note.d.ts +11 -0
- package/dist/knowledge/note.js +39 -0
- package/dist/knowledge/relevance.d.ts +11 -0
- package/dist/knowledge/relevance.js +14 -0
- package/dist/knowledge/span-hash.d.ts +3 -0
- package/dist/knowledge/span-hash.js +16 -0
- package/dist/mcp/auth/access.d.ts +4 -2
- package/dist/mcp/auth/ownership.d.ts +9 -9
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +18 -3
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/host-events/deliveries.d.ts +9 -0
- package/dist/mcp/host-events/deliveries.js +17 -0
- package/dist/mcp/host-events/github-ingress.d.ts +4 -2
- package/dist/mcp/host-events/github-ingress.js +4 -13
- package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
- package/dist/mcp/host-events/jira-ingress.js +92 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +87 -9
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/tools/agents.js +74 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.js +2 -1
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/trigger.js +33 -5
- package/dist/mcp/transport/streamable-http.d.ts +5 -0
- package/dist/mcp/transport/streamable-http.js +23 -1
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/self-hosted.d.ts +8 -1
- package/dist/providers/auth/self-hosted.js +39 -2
- package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/memory-ledger.js +10 -1
- package/dist/providers/coding-proxy/metering.d.ts +2 -1
- package/dist/providers/coding-proxy/metering.js +13 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
- package/dist/providers/coding-proxy/proxy.d.ts +12 -2
- package/dist/providers/coding-proxy/proxy.js +92 -30
- package/dist/providers/coding-proxy/types.d.ts +18 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/executor/composition.js +9 -1
- package/dist/providers/executor/container.d.ts +30 -2
- package/dist/providers/executor/container.js +98 -17
- package/dist/providers/executor/dbos.d.ts +2 -0
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/routing.d.ts +6 -0
- package/dist/providers/executor/routing.js +5 -0
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/issue-tracker/adf.d.ts +31 -0
- package/dist/providers/issue-tracker/adf.js +181 -0
- package/dist/providers/issue-tracker/index.d.ts +5 -0
- package/dist/providers/issue-tracker/index.js +12 -0
- package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
- package/dist/providers/issue-tracker/jira-client.js +151 -0
- package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
- package/dist/providers/issue-tracker/jira-events.js +98 -0
- package/dist/providers/issue-tracker/jira.d.ts +116 -0
- package/dist/providers/issue-tracker/jira.js +502 -0
- package/dist/providers/issue-tracker/types.d.ts +269 -0
- package/dist/providers/issue-tracker/types.js +16 -0
- package/dist/providers/jobs/docker.d.ts +5 -1
- package/dist/providers/jobs/docker.js +61 -33
- package/dist/providers/jobs/kubernetes.d.ts +3 -0
- package/dist/providers/jobs/kubernetes.js +44 -4
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -9
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -9
- package/dist/providers/llm/catalog-lookup.d.ts +10 -0
- package/dist/providers/llm/catalog-lookup.js +15 -0
- package/dist/providers/llm/catalog-shipped.d.ts +18 -0
- package/dist/providers/llm/catalog-shipped.js +197 -0
- package/dist/providers/llm/catalog-store.d.ts +58 -0
- package/dist/providers/llm/catalog-store.js +138 -0
- package/dist/providers/llm/catalog-types.d.ts +66 -0
- package/dist/providers/llm/catalog-types.js +64 -0
- package/dist/providers/llm/catalog.d.ts +61 -0
- package/dist/providers/llm/catalog.js +147 -0
- package/dist/providers/llm/claude-provider.d.ts +10 -14
- package/dist/providers/llm/claude-provider.js +11 -6
- package/dist/providers/llm/index.d.ts +9 -6
- package/dist/providers/llm/index.js +8 -5
- package/dist/providers/llm/openai.d.ts +14 -5
- package/dist/providers/llm/openai.js +24 -14
- package/dist/providers/llm/pricing-core.d.ts +5 -3
- package/dist/providers/llm/registration.js +8 -12
- package/dist/providers/llm/routing.d.ts +18 -17
- package/dist/providers/llm/routing.js +40 -24
- package/dist/providers/review-host/github-events.js +47 -1
- package/dist/providers/review-host/github.js +7 -6
- package/dist/providers/review-host/types.d.ts +25 -0
- package/dist/providers/vcs/git.js +2 -22
- package/dist/providers/vcs/github.d.ts +20 -0
- package/dist/providers/vcs/github.js +28 -2
- package/dist/providers/vcs/types.d.ts +6 -0
- package/dist/quickstart/index.d.ts +8 -0
- package/dist/quickstart/index.js +34 -34
- package/dist/serve.js +8 -2
- package/dist/viewer/api-schema.d.ts +2757 -0
- package/dist/viewer/api-schema.js +165 -0
- package/dist/viewer/build-schemas.d.ts +2 -0
- package/dist/viewer/build-schemas.js +18 -0
- package/dist/viewer/event-bus.d.ts +38 -0
- package/dist/viewer/event-bus.js +232 -0
- package/dist/viewer/graph.d.ts +40 -0
- package/dist/viewer/graph.js +243 -0
- package/dist/viewer/http.d.ts +30 -0
- package/dist/viewer/http.js +133 -0
- package/dist/viewer/run-detail.d.ts +4 -0
- package/dist/viewer/run-detail.js +61 -0
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +10 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/code-review-agents.md +29 -2
- package/docs/coding-agent-setup.md +3 -0
- package/docs/coding-worker-isolation.md +39 -5
- package/docs/getting-started-gke.md +28 -11
- package/docs/getting-started-identity-provider.md +49 -38
- package/docs/getting-started.md +14 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/security-deployment.md +19 -9
- package/docs/viewer-api.md +142 -0
- package/help/admin-viewer.md +39 -0
- package/help/agent-recipes.md +173 -0
- package/help/architecture-agent.md +189 -0
- package/help/builder-agent.md +80 -0
- package/help/code-review-agents.md +6 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +22 -0
- package/help/deploy-gke.md +6 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/getting-started.md +1 -0
- package/help/github.md +18 -0
- package/help/identity-and-access.md +8 -3
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/models.md +90 -0
- package/help/operating-agents.md +7 -1
- package/help/troubleshooting/budgets.md +6 -0
- package/package.json +5 -2
- package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
- package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
- package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
- package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
- package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
- package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
- package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
- package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
- package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
- package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
- package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
- package/prisma/schema.prisma +258 -2
- package/dist/mcp/tools/models.d.ts +0 -8
- package/dist/mcp/tools/models.js +0 -15
- package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
- package/dist/providers/llm/pricing-anthropic.js +0 -48
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
- package/dist/providers/llm/pricing.d.ts +0 -30
- package/dist/providers/llm/pricing.js +0 -74
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The merged model catalog: the shipped catalog (code) overlaid with a
|
|
3
|
+
* deployment's ModelCatalogEntry rows (admins, via set_model). Immutable;
|
|
4
|
+
* CatalogStore swaps in a new one on each refresh. Pure — no I/O.
|
|
5
|
+
*
|
|
6
|
+
* A row replaces the shipped entry with the same (provider, modelId)
|
|
7
|
+
* completely; enabled=false removes the model; a row with no shipped match
|
|
8
|
+
* adds one. Routing is by model id alone, so one model id can belong to only
|
|
9
|
+
* one provider (ownerOf): a row that would claim an id another provider owns
|
|
10
|
+
* is skipped (set_model refuses it at write time; this is the backstop).
|
|
11
|
+
*/
|
|
12
|
+
import { type CatalogEntry, type CatalogRow, type ResolvedCatalogEntry } from "./catalog-types.js";
|
|
13
|
+
export declare class ModelCatalog {
|
|
14
|
+
private readonly active;
|
|
15
|
+
private readonly disabled;
|
|
16
|
+
private readonly shipped;
|
|
17
|
+
constructor(active: ReadonlyMap<string, ResolvedCatalogEntry>, disabled: ReadonlyMap<string, ResolvedCatalogEntry>, shipped: ReadonlyMap<string, CatalogEntry>);
|
|
18
|
+
get(modelId: string): ResolvedCatalogEntry | undefined;
|
|
19
|
+
/** Throws ModelUnavailableError (not_in_catalog | disabled). */
|
|
20
|
+
require(modelId: string): ResolvedCatalogEntry;
|
|
21
|
+
entries(): ResolvedCatalogEntry[];
|
|
22
|
+
disabledEntries(): ResolvedCatalogEntry[];
|
|
23
|
+
shippedEntry(modelId: string): CatalogEntry | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** What deciding a model id's owner needs from one of its ModelCatalogEntry rows. */
|
|
26
|
+
export interface CatalogClaim {
|
|
27
|
+
provider: string;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Orders claims on one model id: the earliest-created row first, ties broken
|
|
33
|
+
* by provider name. createdAt, never updatedAt: an owner editing its own row
|
|
34
|
+
* (set_model, disable_model) must not hand the id to a row claimed after it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function compareClaims(a: CatalogClaim, b: CatalogClaim): number;
|
|
37
|
+
/**
|
|
38
|
+
* The one ownership rule, shared by buildCatalog (the merge) and set_model
|
|
39
|
+
* (its write-time conflict check), so the two can never disagree. A shipped
|
|
40
|
+
* model id ALWAYS belongs to its shipped provider: any row for it under
|
|
41
|
+
* another provider is a dead orphan, however many there are and whenever
|
|
42
|
+
* they were written. A non-shipped id belongs to the provider of its
|
|
43
|
+
* earliest-created row (compareClaims) until reset_model clears every row.
|
|
44
|
+
* `enabled` is the owner's row's (a disabled row still reserves the id); a
|
|
45
|
+
* shipped provider with no row is the untouched, enabled shipped entry.
|
|
46
|
+
* Null only for an id that is neither shipped nor claimed by any row.
|
|
47
|
+
*
|
|
48
|
+
* Which rows count as claims is the caller's call: buildCatalog passes only
|
|
49
|
+
* rows that passed rowFromRecord (CatalogStore drops malformed ones), while
|
|
50
|
+
* set_model passes every raw row, malformed included, so a malformed row
|
|
51
|
+
* still reserves its id until reset_model clears it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ownerOf(shippedEntry: Pick<CatalogEntry, "provider"> | undefined, claims: readonly CatalogClaim[]): {
|
|
54
|
+
provider: string;
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
} | null;
|
|
57
|
+
export declare function buildCatalog(shipped: readonly CatalogEntry[], rows: readonly CatalogRow[], shippedVersion: string, onConflict?: (message: string) => void): ModelCatalog;
|
|
58
|
+
/** The shipped catalog with no overrides (tests, tools without a database, old proxy sessions). */
|
|
59
|
+
export declare function shippedCatalog(): ModelCatalog;
|
|
60
|
+
/** Validates one ModelCatalogEntry record; null (and the caller logs) if it is malformed. */
|
|
61
|
+
export declare function rowFromRecord(record: Record<string, unknown>): CatalogRow | null;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The merged model catalog: the shipped catalog (code) overlaid with a
|
|
3
|
+
* deployment's ModelCatalogEntry rows (admins, via set_model). Immutable;
|
|
4
|
+
* CatalogStore swaps in a new one on each refresh. Pure — no I/O.
|
|
5
|
+
*
|
|
6
|
+
* A row replaces the shipped entry with the same (provider, modelId)
|
|
7
|
+
* completely; enabled=false removes the model; a row with no shipped match
|
|
8
|
+
* adds one. Routing is by model id alone, so one model id can belong to only
|
|
9
|
+
* one provider (ownerOf): a row that would claim an id another provider owns
|
|
10
|
+
* is skipped (set_model refuses it at write time; this is the backstop).
|
|
11
|
+
*/
|
|
12
|
+
import { MODEL_PROVIDERS, ModelUnavailableError, parseStoredEntry, sameEntry, } from "./catalog-types.js";
|
|
13
|
+
import { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
|
|
14
|
+
export class ModelCatalog {
|
|
15
|
+
active;
|
|
16
|
+
disabled;
|
|
17
|
+
shipped;
|
|
18
|
+
constructor(active, disabled, shipped) {
|
|
19
|
+
this.active = active;
|
|
20
|
+
this.disabled = disabled;
|
|
21
|
+
this.shipped = shipped;
|
|
22
|
+
}
|
|
23
|
+
get(modelId) {
|
|
24
|
+
return this.active.get(modelId);
|
|
25
|
+
}
|
|
26
|
+
/** Throws ModelUnavailableError (not_in_catalog | disabled). */
|
|
27
|
+
require(modelId) {
|
|
28
|
+
const entry = this.active.get(modelId);
|
|
29
|
+
if (entry)
|
|
30
|
+
return entry;
|
|
31
|
+
throw new ModelUnavailableError(modelId, this.disabled.has(modelId) ? "disabled" : "not_in_catalog");
|
|
32
|
+
}
|
|
33
|
+
entries() {
|
|
34
|
+
return [...this.active.values()];
|
|
35
|
+
}
|
|
36
|
+
disabledEntries() {
|
|
37
|
+
return [...this.disabled.values()];
|
|
38
|
+
}
|
|
39
|
+
shippedEntry(modelId) {
|
|
40
|
+
return this.shipped.get(modelId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Orders claims on one model id: the earliest-created row first, ties broken
|
|
45
|
+
* by provider name. createdAt, never updatedAt: an owner editing its own row
|
|
46
|
+
* (set_model, disable_model) must not hand the id to a row claimed after it.
|
|
47
|
+
*/
|
|
48
|
+
export function compareClaims(a, b) {
|
|
49
|
+
return a.createdAt.getTime() - b.createdAt.getTime() || a.provider.localeCompare(b.provider);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The one ownership rule, shared by buildCatalog (the merge) and set_model
|
|
53
|
+
* (its write-time conflict check), so the two can never disagree. A shipped
|
|
54
|
+
* model id ALWAYS belongs to its shipped provider: any row for it under
|
|
55
|
+
* another provider is a dead orphan, however many there are and whenever
|
|
56
|
+
* they were written. A non-shipped id belongs to the provider of its
|
|
57
|
+
* earliest-created row (compareClaims) until reset_model clears every row.
|
|
58
|
+
* `enabled` is the owner's row's (a disabled row still reserves the id); a
|
|
59
|
+
* shipped provider with no row is the untouched, enabled shipped entry.
|
|
60
|
+
* Null only for an id that is neither shipped nor claimed by any row.
|
|
61
|
+
*
|
|
62
|
+
* Which rows count as claims is the caller's call: buildCatalog passes only
|
|
63
|
+
* rows that passed rowFromRecord (CatalogStore drops malformed ones), while
|
|
64
|
+
* set_model passes every raw row, malformed included, so a malformed row
|
|
65
|
+
* still reserves its id until reset_model clears it.
|
|
66
|
+
*/
|
|
67
|
+
export function ownerOf(shippedEntry, claims) {
|
|
68
|
+
if (shippedEntry) {
|
|
69
|
+
const ownRow = claims.find((claim) => claim.provider === shippedEntry.provider);
|
|
70
|
+
return { provider: shippedEntry.provider, enabled: ownRow ? ownRow.enabled : true };
|
|
71
|
+
}
|
|
72
|
+
if (claims.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const [first] = [...claims].sort(compareClaims);
|
|
75
|
+
return { provider: first.provider, enabled: first.enabled };
|
|
76
|
+
}
|
|
77
|
+
export function buildCatalog(shipped, rows, shippedVersion, onConflict = () => undefined) {
|
|
78
|
+
const shippedById = new Map(shipped.map((e) => [e.modelId, e]));
|
|
79
|
+
const active = new Map(shipped.map((e) => [e.modelId, { ...e, origin: "shipped", priceVersion: `shipped:${shippedVersion}` }]));
|
|
80
|
+
const disabled = new Map();
|
|
81
|
+
const sorted = [...rows].sort(compareClaims);
|
|
82
|
+
const rowsById = new Map();
|
|
83
|
+
for (const row of sorted)
|
|
84
|
+
rowsById.set(row.modelId, [...(rowsById.get(row.modelId) ?? []), row]);
|
|
85
|
+
for (const row of sorted) {
|
|
86
|
+
const shippedEntry = shippedById.get(row.modelId);
|
|
87
|
+
// ownerOf is never null here (the id has at least this row); the fallback only satisfies the type.
|
|
88
|
+
const owner = ownerOf(shippedEntry, rowsById.get(row.modelId) ?? [row])?.provider ?? row.provider;
|
|
89
|
+
if (owner !== row.provider) {
|
|
90
|
+
onConflict(`ModelCatalogEntry (${row.provider}, ${row.modelId}) skipped: model id already served by provider "${owner}".`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const resolved = {
|
|
94
|
+
provider: row.provider,
|
|
95
|
+
modelId: row.modelId,
|
|
96
|
+
encoding: row.encoding,
|
|
97
|
+
inputPerMTok: row.inputPerMTok,
|
|
98
|
+
outputPerMTok: row.outputPerMTok,
|
|
99
|
+
cachedInputPerMTok: row.cachedInputPerMTok,
|
|
100
|
+
cacheWritePerMTok: row.cacheWritePerMTok,
|
|
101
|
+
efforts: [...row.efforts],
|
|
102
|
+
thinkingMode: row.thinkingMode,
|
|
103
|
+
origin: "override",
|
|
104
|
+
priceVersion: row.updatedAt.toISOString(),
|
|
105
|
+
...(shippedEntry ? { shippedDiffers: !sameEntry(shippedEntry, row) } : {}),
|
|
106
|
+
sourceUrl: row.sourceUrl,
|
|
107
|
+
updatedBy: row.updatedBy,
|
|
108
|
+
updatedAt: row.updatedAt,
|
|
109
|
+
};
|
|
110
|
+
if (row.enabled) {
|
|
111
|
+
active.set(row.modelId, resolved);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
active.delete(row.modelId);
|
|
115
|
+
disabled.set(row.modelId, resolved);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return new ModelCatalog(active, disabled, shippedById);
|
|
119
|
+
}
|
|
120
|
+
let shippedOnly;
|
|
121
|
+
/** The shipped catalog with no overrides (tests, tools without a database, old proxy sessions). */
|
|
122
|
+
export function shippedCatalog() {
|
|
123
|
+
shippedOnly ??= buildCatalog(SHIPPED_CATALOG, [], SHIPPED_CATALOG_VERSION);
|
|
124
|
+
return shippedOnly;
|
|
125
|
+
}
|
|
126
|
+
/** Validates one ModelCatalogEntry record; null (and the caller logs) if it is malformed. */
|
|
127
|
+
export function rowFromRecord(record) {
|
|
128
|
+
const entry = parseStoredEntry(record);
|
|
129
|
+
if (!entry)
|
|
130
|
+
return null;
|
|
131
|
+
if (typeof record.enabled !== "boolean")
|
|
132
|
+
return null;
|
|
133
|
+
if (typeof record.sourceUrl !== "string" || typeof record.updatedBy !== "string")
|
|
134
|
+
return null;
|
|
135
|
+
if (!(record.createdAt instanceof Date) || !(record.updatedAt instanceof Date))
|
|
136
|
+
return null;
|
|
137
|
+
if (!MODEL_PROVIDERS.includes(entry.provider))
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
...entry,
|
|
141
|
+
enabled: record.enabled,
|
|
142
|
+
sourceUrl: record.sourceUrl,
|
|
143
|
+
updatedBy: record.updatedBy,
|
|
144
|
+
createdAt: record.createdAt,
|
|
145
|
+
updatedAt: record.updatedAt,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LlmProvider implementation shared by every Claude-protocol adapter (direct
|
|
3
3
|
* Anthropic API, Bedrock-Claude, and any future Claude-protocol client).
|
|
4
|
-
* Imports no SDK — the client and
|
|
4
|
+
* Imports no SDK — the client and catalog lookup are injected, so this file
|
|
5
5
|
* stays exactly what each concrete adapter (anthropic.ts, bedrock.ts) has in
|
|
6
6
|
* common, and nothing more.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
import
|
|
8
|
+
import type { LlmMessage, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
|
|
9
|
+
import { type CatalogLookup } from "./catalog-lookup.js";
|
|
10
|
+
import type { CatalogEntry } from "./catalog-types.js";
|
|
11
|
+
import type { CatalogLlmAdapter } from "./routing.js";
|
|
10
12
|
/**
|
|
11
13
|
* Structural shape every Claude-protocol SDK client satisfies. Loose on
|
|
12
14
|
* purpose — params/return are untyped here, mirroring the cast anthropic.ts
|
|
@@ -20,17 +22,11 @@ export interface ClaudeMessagesClient {
|
|
|
20
22
|
}): unknown;
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
supportedEfforts(model: string): readonly LlmEffort[];
|
|
29
|
-
}
|
|
30
|
-
export declare class ClaudeLlmProvider implements LlmProvider {
|
|
31
|
-
private readonly client;
|
|
32
|
-
private readonly pricing;
|
|
33
|
-
constructor(client: ClaudeMessagesClient, pricing: ClaudePricingModule);
|
|
25
|
+
export declare class ClaudeLlmProvider implements CatalogLlmAdapter {
|
|
26
|
+
protected readonly client: ClaudeMessagesClient;
|
|
27
|
+
private readonly lookup;
|
|
28
|
+
constructor(client: ClaudeMessagesClient, lookup?: CatalogLookup);
|
|
29
|
+
withEntry(entry: CatalogEntry): ClaudeLlmProvider;
|
|
34
30
|
stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
|
|
35
31
|
countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
|
|
36
32
|
priceUsd(model: string, usage: {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { toClaudeRequest, withCacheBreakpoints, mapClaudeStream, estimateClaudeTokens, } from "./claude-messages.js";
|
|
2
|
+
import { computeCost } from "./pricing-core.js";
|
|
3
|
+
import { currentLookup, pinnedLookup } from "./catalog-lookup.js";
|
|
2
4
|
// Anthropic's own guidance: don't lowball max_tokens — hitting the cap
|
|
3
5
|
// truncates output mid-thought with no error, silently handing engine-native.ts
|
|
4
6
|
// (which never sets LlmRequest.maxTokens, and never checks stopReason) a
|
|
@@ -11,25 +13,28 @@ const DEFAULT_MAX_TOKENS = 16000;
|
|
|
11
13
|
const CLAUDE_TOKEN_INFLATION = 1.2;
|
|
12
14
|
export class ClaudeLlmProvider {
|
|
13
15
|
client;
|
|
14
|
-
|
|
15
|
-
constructor(client,
|
|
16
|
+
lookup;
|
|
17
|
+
constructor(client, lookup = currentLookup) {
|
|
16
18
|
this.client = client;
|
|
17
|
-
this.
|
|
19
|
+
this.lookup = lookup;
|
|
20
|
+
}
|
|
21
|
+
withEntry(entry) {
|
|
22
|
+
return new ClaudeLlmProvider(this.client, pinnedLookup(entry));
|
|
18
23
|
}
|
|
19
24
|
async *stream(req, signal) {
|
|
20
25
|
// Defence in depth behind config-time validation: a level the model
|
|
21
26
|
// doesn't accept would fail the whole call, so it is dropped here.
|
|
22
27
|
const { effort, ...rest } = req;
|
|
23
|
-
const accepted = effort && this.
|
|
28
|
+
const accepted = effort && this.lookup(req.model).efforts.includes(effort);
|
|
24
29
|
const claudeReq = withCacheBreakpoints(toClaudeRequest(accepted ? { ...rest, effort } : rest, DEFAULT_MAX_TOKENS));
|
|
25
30
|
const raw = this.client.messages.stream({ model: req.model, ...claudeReq }, { signal });
|
|
26
31
|
yield* mapClaudeStream(raw, (usage) => this.priceUsd(req.model, usage));
|
|
27
32
|
}
|
|
28
33
|
async countTokens(model, messages, tools) {
|
|
29
|
-
this.
|
|
34
|
+
this.lookup(model); // fail-closed on a model the catalog does not serve
|
|
30
35
|
return Math.ceil(estimateClaudeTokens(messages, tools) * CLAUDE_TOKEN_INFLATION);
|
|
31
36
|
}
|
|
32
37
|
priceUsd(model, usage) {
|
|
33
|
-
return this.
|
|
38
|
+
return computeCost(this.lookup(model), usage);
|
|
34
39
|
}
|
|
35
40
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
export * from "./pricing.js";
|
|
2
|
+
export * from "./pricing-core.js";
|
|
3
3
|
export * from "./openai.js";
|
|
4
|
-
export { AnthropicLlmProvider, anthropicCredentialsPresent
|
|
5
|
-
export { BedrockClaudeLlmProvider, bedrockCredentialsPresent
|
|
6
|
-
export { ClaudeLlmProvider, type ClaudeMessagesClient
|
|
4
|
+
export { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
|
|
5
|
+
export { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
|
|
6
|
+
export { ClaudeLlmProvider, type ClaudeMessagesClient } from "./claude-provider.js";
|
|
7
7
|
export { openaiCredentialsPresent } from "./openai.js";
|
|
8
|
-
export {
|
|
9
|
-
export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, type LlmRegistration } from "./routing.js";
|
|
8
|
+
export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, type CatalogLlmAdapter, type LlmRegistration, } from "./routing.js";
|
|
10
9
|
export { resolveLlmRegistrations, type LlmRegistrationResult } from "./registration.js";
|
|
10
|
+
export * from "./catalog-types.js";
|
|
11
|
+
export { ModelCatalog, buildCatalog, shippedCatalog } from "./catalog.js";
|
|
12
|
+
export { CatalogStore, currentModelCatalog, installModelCatalog, refreshIntervalMs } from "./catalog-store.js";
|
|
13
|
+
export { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
export * from "./pricing.js";
|
|
2
|
+
export * from "./pricing-core.js";
|
|
3
3
|
export * from "./openai.js";
|
|
4
|
-
export { AnthropicLlmProvider, anthropicCredentialsPresent
|
|
5
|
-
export { BedrockClaudeLlmProvider, bedrockCredentialsPresent
|
|
4
|
+
export { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
|
|
5
|
+
export { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
|
|
6
6
|
export { ClaudeLlmProvider } from "./claude-provider.js";
|
|
7
7
|
export { openaiCredentialsPresent } from "./openai.js";
|
|
8
|
-
export {
|
|
9
|
-
export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts } from "./routing.js";
|
|
8
|
+
export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, } from "./routing.js";
|
|
10
9
|
export { resolveLlmRegistrations } from "./registration.js";
|
|
10
|
+
export * from "./catalog-types.js";
|
|
11
|
+
export { ModelCatalog, buildCatalog, shippedCatalog } from "./catalog.js";
|
|
12
|
+
export { CatalogStore, currentModelCatalog, installModelCatalog, refreshIntervalMs } from "./catalog-store.js";
|
|
13
|
+
export { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
|
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
* never imports it.
|
|
5
5
|
*/
|
|
6
6
|
import OpenAI from "openai";
|
|
7
|
-
import type { LlmMessage,
|
|
7
|
+
import type { LlmMessage, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
|
|
8
|
+
import { type CatalogLookup } from "./catalog-lookup.js";
|
|
9
|
+
import type { CatalogEntry } from "./catalog-types.js";
|
|
10
|
+
import type { CatalogLlmAdapter } from "./routing.js";
|
|
8
11
|
/** Whether the OpenAI adapter has credentials to run (used by the router's enable-by-credential wiring). */
|
|
9
12
|
export declare function openaiCredentialsPresent(env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
-
export declare function estimateTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): number;
|
|
11
|
-
export declare class OpenAiLlmProvider implements
|
|
13
|
+
export declare function estimateTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[], lookup?: CatalogLookup): number;
|
|
14
|
+
export declare class OpenAiLlmProvider implements CatalogLlmAdapter {
|
|
15
|
+
private readonly lookup;
|
|
12
16
|
private readonly client;
|
|
13
|
-
/**
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `client` is an injection point for tests and withEntry — a real adapter
|
|
19
|
+
* never passes it. `lookup` is a parameter property so it is set before the
|
|
20
|
+
* early return below.
|
|
21
|
+
*/
|
|
22
|
+
constructor(apiKey?: string, client?: OpenAI, lookup?: CatalogLookup);
|
|
23
|
+
withEntry(entry: CatalogEntry): OpenAiLlmProvider;
|
|
15
24
|
stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
|
|
16
25
|
countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
|
|
17
26
|
priceUsd(model: string, usage: {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import OpenAI from "openai";
|
|
7
7
|
import { encode as encodeCl100kBase } from "gpt-tokenizer/encoding/cl100k_base";
|
|
8
8
|
import { encode as encodeO200kBase } from "gpt-tokenizer/encoding/o200k_base";
|
|
9
|
-
import {
|
|
9
|
+
import { computeCost } from "./pricing-core.js";
|
|
10
|
+
import { currentLookup, pinnedLookup } from "./catalog-lookup.js";
|
|
10
11
|
/** Whether the OpenAI adapter has credentials to run (used by the router's enable-by-credential wiring). */
|
|
11
12
|
export function openaiCredentialsPresent(env = process.env) {
|
|
12
13
|
return Boolean(env.OPENAI_API_KEY);
|
|
@@ -22,10 +23,10 @@ const TOKENS_PRIMING_REPLY = 3;
|
|
|
22
23
|
// gpt-tokenizer's package default is cl100k_base, but every gpt-4o-and-later
|
|
23
24
|
// model (all of Phase 1's roster) actually uses o200k_base — using the
|
|
24
25
|
// wrong table skews the pre-flight token count, which is the budget
|
|
25
|
-
// guardrail's input. The encoding lives on the
|
|
26
|
-
//
|
|
27
|
-
function encodeForModel(model, text) {
|
|
28
|
-
const { encoding } =
|
|
26
|
+
// guardrail's input. The encoding lives on the catalog entry, so a model
|
|
27
|
+
// can't be run without also declaring which tokenizer counts it.
|
|
28
|
+
function encodeForModel(model, text, lookup) {
|
|
29
|
+
const { encoding } = lookup(model);
|
|
29
30
|
return encoding === "o200k_base" ? encodeO200kBase(text) : encodeCl100kBase(text);
|
|
30
31
|
}
|
|
31
32
|
/** Same shape sent to the API in `stream()` — kept as one function so the estimate can never drift from what's actually serialized. */
|
|
@@ -44,25 +45,31 @@ function toOpenAiTools(tools) {
|
|
|
44
45
|
// is still an *approximation* (not OpenAI's undocumented exact tool-schema
|
|
45
46
|
// tokenization), not a byte-for-byte match — calibration logging
|
|
46
47
|
// (budget.ts's checkTokenCalibration) is what surfaces any remaining drift.
|
|
47
|
-
export function estimateTokens(model, messages, tools) {
|
|
48
|
+
export function estimateTokens(model, messages, tools, lookup = currentLookup) {
|
|
48
49
|
let total = TOKENS_PRIMING_REPLY;
|
|
49
50
|
for (const message of messages) {
|
|
50
51
|
total += TOKENS_PER_MESSAGE;
|
|
51
|
-
total += encodeForModel(model, message.content).length;
|
|
52
|
-
total += encodeForModel(model, message.role).length;
|
|
52
|
+
total += encodeForModel(model, message.content, lookup).length;
|
|
53
|
+
total += encodeForModel(model, message.role, lookup).length;
|
|
53
54
|
if (message.name) {
|
|
54
|
-
total += encodeForModel(model, message.name).length + TOKENS_PER_NAME;
|
|
55
|
+
total += encodeForModel(model, message.name, lookup).length + TOKENS_PER_NAME;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
if (tools && tools.length > 0) {
|
|
58
|
-
total += encodeForModel(model, JSON.stringify(toOpenAiTools(tools))).length;
|
|
59
|
+
total += encodeForModel(model, JSON.stringify(toOpenAiTools(tools)), lookup).length;
|
|
59
60
|
}
|
|
60
61
|
return total;
|
|
61
62
|
}
|
|
62
63
|
export class OpenAiLlmProvider {
|
|
64
|
+
lookup;
|
|
63
65
|
client;
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
+
/**
|
|
67
|
+
* `client` is an injection point for tests and withEntry — a real adapter
|
|
68
|
+
* never passes it. `lookup` is a parameter property so it is set before the
|
|
69
|
+
* early return below.
|
|
70
|
+
*/
|
|
71
|
+
constructor(apiKey = process.env.OPENAI_API_KEY ?? "", client, lookup = currentLookup) {
|
|
72
|
+
this.lookup = lookup;
|
|
66
73
|
if (client) {
|
|
67
74
|
this.client = client;
|
|
68
75
|
return;
|
|
@@ -72,6 +79,9 @@ export class OpenAiLlmProvider {
|
|
|
72
79
|
}
|
|
73
80
|
this.client = new OpenAI({ apiKey });
|
|
74
81
|
}
|
|
82
|
+
withEntry(entry) {
|
|
83
|
+
return new OpenAiLlmProvider("", this.client, pinnedLookup(entry));
|
|
84
|
+
}
|
|
75
85
|
async *stream(req, signal) {
|
|
76
86
|
const stream = await this.client.chat.completions.create({
|
|
77
87
|
model: req.model,
|
|
@@ -148,9 +158,9 @@ export class OpenAiLlmProvider {
|
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
async countTokens(model, messages, tools) {
|
|
151
|
-
return estimateTokens(model, messages, tools);
|
|
161
|
+
return estimateTokens(model, messages, tools, this.lookup);
|
|
152
162
|
}
|
|
153
163
|
priceUsd(model, usage) {
|
|
154
|
-
return
|
|
164
|
+
return computeCost(this.lookup(model), usage);
|
|
155
165
|
}
|
|
156
166
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared pricing types + cost math, provider-agnostic.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Shared pricing types + cost math, provider-agnostic. Rates come from the
|
|
3
|
+
* model catalog (docs/models.md: the shipped catalog plus this deployment's
|
|
4
|
+
* set_model overrides), not from a per-adapter table; every provider prices
|
|
5
|
+
* through this one `computeCost`, so the cache-read/cache-write accounting
|
|
6
|
+
* can never diverge between providers.
|
|
5
7
|
*/
|
|
6
8
|
export type TokenizerEncoding = "cl100k_base" | "o200k_base";
|
|
7
9
|
export interface ModelPricing {
|
|
@@ -10,30 +10,26 @@
|
|
|
10
10
|
* OpenAI, direct Anthropic, and Bedrock-Claude can all be active in one
|
|
11
11
|
* deployment at once. An agent's `model` field (routed by RoutingLlmProvider)
|
|
12
12
|
* picks which one handles its calls; there is no exclusive global mode.
|
|
13
|
+
* A registration names only its provider — models come from the catalog.
|
|
13
14
|
*/
|
|
14
15
|
import { OpenAiLlmProvider, openaiCredentialsPresent } from "./openai.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { BedrockClaudeLlmProvider, bedrockCredentialsPresent, bedrockClaudeSupportedModels } from "./bedrock.js";
|
|
16
|
+
import { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
|
|
17
|
+
import { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
|
|
18
18
|
export function resolveLlmRegistrations(env = process.env) {
|
|
19
19
|
const registrations = [];
|
|
20
20
|
if (openaiCredentialsPresent(env)) {
|
|
21
|
-
registrations.push({ provider: new OpenAiLlmProvider(env.OPENAI_API_KEY)
|
|
21
|
+
registrations.push({ provider: "openai", adapter: new OpenAiLlmProvider(env.OPENAI_API_KEY) });
|
|
22
22
|
}
|
|
23
23
|
if (anthropicCredentialsPresent(env)) {
|
|
24
|
-
registrations.push({
|
|
25
|
-
provider: new AnthropicLlmProvider(env.ANTHROPIC_API_KEY),
|
|
26
|
-
models: anthropicSupportedModels(),
|
|
27
|
-
});
|
|
24
|
+
registrations.push({ provider: "anthropic", adapter: new AnthropicLlmProvider(env.ANTHROPIC_API_KEY) });
|
|
28
25
|
}
|
|
29
26
|
if (bedrockCredentialsPresent(env)) {
|
|
30
27
|
registrations.push({
|
|
31
|
-
provider:
|
|
32
|
-
|
|
28
|
+
provider: "bedrock-claude",
|
|
29
|
+
adapter: new BedrockClaudeLlmProvider(env.BEDROCK_REGION ?? env.AWS_REGION),
|
|
33
30
|
});
|
|
34
31
|
}
|
|
35
|
-
if (registrations.length === 0)
|
|
32
|
+
if (registrations.length === 0)
|
|
36
33
|
return { kind: "no-credentials" };
|
|
37
|
-
}
|
|
38
34
|
return { kind: "registrations", registrations };
|
|
39
35
|
}
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* dispatches each call to the adapter that owns `model`, so per-agent model
|
|
4
|
-
* selection (Agent.model) selects the provider too — GPT and Claude agents
|
|
5
|
-
* run in one deployment. Unknown model / duplicate registration fail closed.
|
|
6
|
-
*/
|
|
1
|
+
import type { ModelCatalog } from "./catalog.js";
|
|
2
|
+
import { type CatalogEntry, type ModelProvider, type ResolvedCatalogEntry } from "./catalog-types.js";
|
|
7
3
|
import type { LlmEffort, LlmMessage, LlmProvider, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
|
|
8
|
-
/**
|
|
9
|
-
* Effort levels `model` accepts, whichever provider owns it — for config-time
|
|
10
|
-
* validation, so it needs no credentials. Each provider's own table is the
|
|
11
|
-
* source of truth. OpenAI models accept none: the Chat Completions
|
|
12
|
-
* `reasoning_effort` parameter is documented for o-series models only, and
|
|
13
|
-
* none is on the roster.
|
|
14
|
-
*/
|
|
4
|
+
/** Effort levels `model` accepts, from the current catalog; no credentials needed (config-time validation). */
|
|
15
5
|
export declare function modelSupportedEfforts(model: string): readonly LlmEffort[];
|
|
16
6
|
export declare function modelAcceptsEffort(model: string, effort: LlmEffort): boolean;
|
|
7
|
+
/** An adapter that can be pinned to one run's stored catalog entry. */
|
|
8
|
+
export interface CatalogLlmAdapter extends LlmProvider {
|
|
9
|
+
withEntry(entry: CatalogEntry): LlmProvider;
|
|
10
|
+
}
|
|
17
11
|
export interface LlmRegistration {
|
|
18
|
-
provider:
|
|
19
|
-
|
|
12
|
+
provider: ModelProvider;
|
|
13
|
+
adapter: CatalogLlmAdapter;
|
|
20
14
|
}
|
|
21
15
|
export declare class RoutingLlmProvider implements LlmProvider {
|
|
22
|
-
private readonly
|
|
23
|
-
|
|
16
|
+
private readonly catalog;
|
|
17
|
+
private readonly byProvider;
|
|
18
|
+
constructor(registrations: LlmRegistration[], catalog?: () => ModelCatalog);
|
|
19
|
+
hasProvider(provider: ModelProvider): boolean;
|
|
20
|
+
/** Model ids this deployment can run right now. */
|
|
24
21
|
listModels(): string[];
|
|
22
|
+
/** The current catalog entry for a runnable model; throws ModelUnavailableError otherwise. */
|
|
23
|
+
entryFor(model: string): ResolvedCatalogEntry;
|
|
24
|
+
/** A provider bound to one run's stored entry: prices and shapes requests from it, never the live catalog. */
|
|
25
|
+
forRun(entry: CatalogEntry): LlmProvider;
|
|
25
26
|
private resolve;
|
|
26
27
|
stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
|
|
27
28
|
countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
|
|
@@ -1,42 +1,58 @@
|
|
|
1
|
-
import { anthropicSupportedEfforts } from "./pricing-anthropic.js";
|
|
2
|
-
import { bedrockClaudeSupportedEfforts } from "./pricing-bedrock-claude.js";
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Model->provider routing over the model catalog. Becomes the single
|
|
3
|
+
* ProviderRegistry.llm: each call's model is looked up in the current
|
|
4
|
+
* catalog, and the adapter registered for that entry's provider handles it.
|
|
5
|
+
* A model is routable when it is in the catalog AND its provider has
|
|
6
|
+
* credentials (registration.ts). Unknown model / unconfigured provider /
|
|
7
|
+
* duplicate registration all fail closed.
|
|
9
8
|
*/
|
|
9
|
+
import { currentModelCatalog } from "./catalog-store.js";
|
|
10
|
+
import { ModelUnavailableError, } from "./catalog-types.js";
|
|
11
|
+
/** Effort levels `model` accepts, from the current catalog; no credentials needed (config-time validation). */
|
|
10
12
|
export function modelSupportedEfforts(model) {
|
|
11
|
-
|
|
12
|
-
if (direct.length > 0)
|
|
13
|
-
return direct;
|
|
14
|
-
return bedrockClaudeSupportedEfforts(model);
|
|
13
|
+
return currentModelCatalog().get(model)?.efforts ?? [];
|
|
15
14
|
}
|
|
16
15
|
export function modelAcceptsEffort(model, effort) {
|
|
17
16
|
return modelSupportedEfforts(model).includes(effort);
|
|
18
17
|
}
|
|
19
18
|
export class RoutingLlmProvider {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
catalog;
|
|
20
|
+
byProvider = new Map();
|
|
21
|
+
constructor(registrations, catalog = currentModelCatalog) {
|
|
22
|
+
this.catalog = catalog;
|
|
22
23
|
for (const reg of registrations) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
throw new Error(`Model "${model}" is registered by more than one LLM provider — check the routing configuration.`);
|
|
26
|
-
}
|
|
27
|
-
this.byModel.set(model, reg.provider);
|
|
24
|
+
if (this.byProvider.has(reg.provider)) {
|
|
25
|
+
throw new Error(`LLM provider "${reg.provider}" is registered more than once — check the routing configuration.`);
|
|
28
26
|
}
|
|
27
|
+
this.byProvider.set(reg.provider, reg.adapter);
|
|
29
28
|
}
|
|
30
29
|
}
|
|
30
|
+
hasProvider(provider) {
|
|
31
|
+
return this.byProvider.has(provider);
|
|
32
|
+
}
|
|
33
|
+
/** Model ids this deployment can run right now. */
|
|
31
34
|
listModels() {
|
|
32
|
-
return
|
|
35
|
+
return this.catalog()
|
|
36
|
+
.entries()
|
|
37
|
+
.filter((e) => this.byProvider.has(e.provider))
|
|
38
|
+
.map((e) => e.modelId);
|
|
39
|
+
}
|
|
40
|
+
/** The current catalog entry for a runnable model; throws ModelUnavailableError otherwise. */
|
|
41
|
+
entryFor(model) {
|
|
42
|
+
const entry = this.catalog().require(model);
|
|
43
|
+
if (!this.byProvider.has(entry.provider))
|
|
44
|
+
throw new ModelUnavailableError(model, "provider_not_configured");
|
|
45
|
+
return entry;
|
|
46
|
+
}
|
|
47
|
+
/** A provider bound to one run's stored entry: prices and shapes requests from it, never the live catalog. */
|
|
48
|
+
forRun(entry) {
|
|
49
|
+
const adapter = this.byProvider.get(entry.provider);
|
|
50
|
+
if (!adapter)
|
|
51
|
+
throw new ModelUnavailableError(entry.modelId, "provider_not_configured");
|
|
52
|
+
return adapter.withEntry(entry);
|
|
33
53
|
}
|
|
34
54
|
resolve(model) {
|
|
35
|
-
|
|
36
|
-
if (!provider) {
|
|
37
|
-
throw new Error(`No LLM provider is registered for model "${model}". Known models: ${[...this.byModel.keys()].join(", ") || "(none)"}.`);
|
|
38
|
-
}
|
|
39
|
-
return provider;
|
|
55
|
+
return this.byProvider.get(this.entryFor(model).provider);
|
|
40
56
|
}
|
|
41
57
|
stream(req, signal) {
|
|
42
58
|
return this.resolve(req.model).stream(req, signal);
|