@loopingai/core 0.7.1 → 0.8.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.
@@ -1,130 +0,0 @@
1
- import Anthropic from "@anthropic-ai/sdk";
2
- import { createAnthropicLanguageModel } from "./language-model.js";
3
- /**
4
- * The beta Anthropic requires to accept a bearer credential rather than an
5
- * `x-api-key`.
6
- */
7
- const OAUTH_BETA = "oauth-2025-04-20";
8
- /**
9
- * AI Gateway's per-request log metadata header.
10
- *
11
- * Core already stamps `{taskId, round}` on a turn and `{taskId, subtaskId}` on a
12
- * subagent chunk so a model call can be tied back to the work that made it; the
13
- * Workers AI provider carries that in its settings object. Anthropic has no such
14
- * field, so it rides as a header instead — same correlation, different envelope.
15
- * Capped at five entries by the gateway; extra keys are dropped there, so trim
16
- * here rather than sending something that silently truncates.
17
- */
18
- const GATEWAY_METADATA_HEADER = "cf-aig-metadata";
19
- const GATEWAY_METADATA_MAX_ENTRIES = 5;
20
- /**
21
- * AI Gateway's own authorization header — deliberately *not* `Authorization`,
22
- * which already carries the model provider's credential on the same request.
23
- * Two independent authorities, two headers.
24
- *
25
- * A client header rather than a per-pair one: it authenticates the caller to the
26
- * gateway, which does not vary by task or round, unlike
27
- * {@link GATEWAY_METADATA_HEADER}.
28
- */
29
- const GATEWAY_AUTH_HEADER = "cf-aig-authorization";
30
- function metadataHeaders(metadata) {
31
- if (!metadata)
32
- return {};
33
- const entries = Object.entries(metadata).slice(0, GATEWAY_METADATA_MAX_ENTRIES);
34
- if (entries.length === 0)
35
- return {};
36
- return {
37
- [GATEWAY_METADATA_HEADER]: JSON.stringify(Object.fromEntries(entries))
38
- };
39
- }
40
- export function createAnthropicModelRuntime(deps) {
41
- const { config } = deps;
42
- /**
43
- * The resolved base URL, memoized — **not** the client.
44
- *
45
- * Resolving is what is expensive and what must not happen at module scope:
46
- * `wrangler deploy` evaluates module scope to validate the new version and
47
- * bindings are unpopulated at that point, so an eager `baseUrl()` would throw
48
- * during deploy. That argument is about the *URL*, which is why it is the URL
49
- * that is cached.
50
- *
51
- * Assigned only on success, so a failed gateway lookup is retried on the next
52
- * round rather than cached as a rejected promise for the life of the isolate.
53
- * Two concurrent first calls may each resolve one; they are identical, and
54
- * the cost is a spare string.
55
- */
56
- let resolvedBaseUrl;
57
- /**
58
- * A client per call, deliberately.
59
- *
60
- * This used to memoize the `Anthropic` instance, which bakes the credential
61
- * in at construction. That is correct only while the credential outlives the
62
- * isolate. It does not when {@link AnthropicRuntimeDeps.authToken} mints a
63
- * short-lived token per request: the first call succeeds, and every call
64
- * after the token's lifetime gets a `401` — intermittently, only under
65
- * sustained load, and pointing at the wrong secret.
66
- *
67
- * Rebuilding is close to free. `new Anthropic({...})` is pure config
68
- * assembly — no network, no handshake — so the per-call cost is an object
69
- * allocation, against a correctness bug that only appears in production.
70
- * The per-pair headers ride on the request rather than the client, so nothing
71
- * else depended on the instance being shared.
72
- */
73
- const clientFor = async () => {
74
- if (deps.clientOverride)
75
- return deps.clientOverride;
76
- // Awaited, not cast. `env.AI.gateway(id).getUrl()` returns a PROMISE:
77
- // handing it to `baseURL` unresolved type-checks only behind an `as
78
- // string`, then fails deep inside the SDK on `baseURL.endsWith is not a
79
- // function`, on every request, with nothing naming the gateway. The await
80
- // is why this thunk is async and why `client` is awaited at the call site.
81
- resolvedBaseUrl ??= await deps.baseUrl();
82
- const gatewayToken = deps.gatewayToken?.();
83
- return new Anthropic({
84
- authToken: await deps.authToken(),
85
- // Explicitly null, or the SDK falls back to resolving credentials from
86
- // config files and env vars — which on Workers means a confusing failure
87
- // far from the actual misconfiguration.
88
- apiKey: null,
89
- baseURL: resolvedBaseUrl,
90
- defaultHeaders: {
91
- "anthropic-beta": OAUTH_BETA,
92
- ...(gatewayToken
93
- ? { [GATEWAY_AUTH_HEADER]: `Bearer ${gatewayToken}` }
94
- : {})
95
- },
96
- // Retry lives one layer up, in the AI SDK, which is the only layer that
97
- // honours the provider's own `retry-after` — see `ModelConfig.maxRetries`
98
- // and the `APICallError` mapping that feeds it. A second layer here would
99
- // multiply that wait and defeat the fallback's timing.
100
- maxRetries: 0
101
- });
102
- };
103
- return {
104
- createModelPair(overrides = {}) {
105
- const primaryId = overrides.primaryModelId ?? config.chatModelId;
106
- const fallbackId = overrides.fallbackModelId ?? config.fallbackChatModelId;
107
- const headers = metadataHeaders(overrides.metadata);
108
- const build = (modelId) => createAnthropicLanguageModel({
109
- client: clientFor,
110
- modelId,
111
- defaultMaxTokens: config.maxOutputTokens,
112
- ...(deps.effort ? { effort: deps.effort } : {}),
113
- ...(deps.cache !== undefined ? { cache: deps.cache } : {}),
114
- ...(deps.classifyAuthFailure
115
- ? { classifyAuthFailure: deps.classifyAuthFailure }
116
- : {}),
117
- headers
118
- });
119
- let primary;
120
- let fallback;
121
- return {
122
- primary: () => (primary ??= overrides.model ?? build(primaryId)),
123
- fallback: () => (fallback ??=
124
- overrides.fallbackModel ?? overrides.model ?? build(fallbackId)),
125
- primaryId: () => primaryId,
126
- fallbackId: () => fallbackId
127
- };
128
- }
129
- };
130
- }