@xandreed/plugin-models 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/anthropicOAuth.d.ts +45 -11
  3. package/dist/auth/anthropicOAuth.js +23 -23
  4. package/dist/auth/auth-store.conformance.d.ts +63 -0
  5. package/dist/auth/auth-store.conformance.js +152 -0
  6. package/dist/auth/credential-input.entity.d.ts +52 -0
  7. package/dist/auth/credential-input.entity.functions.d.ts +18 -0
  8. package/dist/auth/credential-input.entity.functions.js +12 -0
  9. package/dist/auth/credential-input.entity.js +22 -0
  10. package/dist/auth/credential-refresh.entity.d.ts +17 -0
  11. package/dist/auth/credential-refresh.entity.functions.d.ts +57 -0
  12. package/dist/auth/credential-refresh.entity.functions.js +21 -0
  13. package/dist/auth/credential-refresh.entity.js +2 -0
  14. package/dist/auth/localAuth.d.ts +12 -2
  15. package/dist/auth/localAuth.js +58 -86
  16. package/dist/auth/memory-auth.adapter.d.ts +35 -0
  17. package/dist/auth/memory-auth.adapter.js +35 -0
  18. package/dist/auth/openAiCodexOAuth.d.ts +28 -6
  19. package/dist/auth/openAiCodexOAuth.js +22 -32
  20. package/dist/auth/token-response.entity.d.ts +21 -0
  21. package/dist/auth/token-response.entity.functions.d.ts +20 -0
  22. package/dist/auth/token-response.entity.functions.js +10 -0
  23. package/dist/auth/token-response.entity.js +10 -0
  24. package/dist/compat/index.d.ts +2 -0
  25. package/dist/compat/index.js +1 -0
  26. package/dist/conformance/index.d.ts +6 -0
  27. package/dist/conformance/index.js +3 -0
  28. package/dist/index.d.ts +30 -13
  29. package/dist/index.js +25 -50
  30. package/dist/llm/chat-wire.entity.d.ts +123 -0
  31. package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
  32. package/dist/llm/chat-wire.entity.functions.js +10 -0
  33. package/dist/llm/chat-wire.entity.js +37 -0
  34. package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
  35. package/dist/llm/compat-telemetry.adapter.js +21 -0
  36. package/dist/llm/compat.d.ts +84 -33
  37. package/dist/llm/compat.js +188 -121
  38. package/dist/llm/model-completion.adapter.d.ts +49 -0
  39. package/dist/llm/model-completion.adapter.js +77 -0
  40. package/dist/llm/model-completion.entity.d.ts +4 -0
  41. package/dist/llm/model-completion.entity.functions.d.ts +3 -0
  42. package/dist/llm/model-completion.entity.functions.js +7 -0
  43. package/dist/llm/model-completion.entity.js +7 -0
  44. package/dist/llm/model-retry-policy.entity.d.ts +37 -0
  45. package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
  46. package/dist/llm/model-retry-policy.entity.functions.js +33 -0
  47. package/dist/llm/model-retry-policy.entity.js +23 -0
  48. package/dist/llm/model-settings.entity.d.ts +85 -0
  49. package/dist/llm/model-settings.entity.functions.d.ts +11 -0
  50. package/dist/llm/model-settings.entity.functions.js +49 -0
  51. package/dist/llm/model-settings.entity.js +28 -0
  52. package/dist/llm/model-usage.entity.d.ts +61 -0
  53. package/dist/llm/model-usage.entity.functions.d.ts +11 -0
  54. package/dist/llm/model-usage.entity.functions.js +24 -0
  55. package/dist/llm/model-usage.entity.js +19 -0
  56. package/dist/llm/modelCatalog.d.ts +47 -7
  57. package/dist/llm/modelCatalog.js +31 -57
  58. package/dist/llm/openAiCodex.d.ts +18 -7
  59. package/dist/llm/openAiCodex.js +57 -45
  60. package/dist/llm/openAiCodexWebSocket.d.ts +60 -7
  61. package/dist/llm/openAiCodexWebSocket.js +113 -105
  62. package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
  63. package/dist/llm/prompt-cache-frame.entity.js +10 -0
  64. package/dist/llm/prompt-cache.entity.d.ts +57 -0
  65. package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
  66. package/dist/llm/prompt-cache.entity.functions.js +73 -0
  67. package/dist/llm/prompt-cache.entity.js +27 -0
  68. package/dist/llm/provider-client.adapter.d.ts +9 -0
  69. package/dist/llm/provider-client.adapter.js +60 -0
  70. package/dist/llm/provider-failure.entity.d.ts +42 -0
  71. package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
  72. package/dist/llm/provider-failure.entity.functions.js +67 -0
  73. package/dist/llm/provider-failure.entity.js +20 -0
  74. package/dist/llm/provider-profile.entity.d.ts +79 -0
  75. package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
  76. package/dist/llm/provider-profile.entity.functions.js +83 -0
  77. package/dist/llm/provider-profile.entity.js +10 -0
  78. package/dist/llm/providers.d.ts +125 -29
  79. package/dist/llm/providers.js +311 -117
  80. package/dist/llm/retry.d.ts +11 -47
  81. package/dist/llm/retry.js +33 -52
  82. package/dist/llm/router.d.ts +56 -28
  83. package/dist/llm/router.js +125 -88
  84. package/dist/llm/sse.d.ts +58 -29
  85. package/dist/llm/sse.js +47 -39
  86. package/dist/llm/thinking-binding.entity.d.ts +49 -0
  87. package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
  88. package/dist/llm/thinking-binding.entity.functions.js +16 -0
  89. package/dist/llm/thinking-binding.entity.js +11 -0
  90. package/dist/llm/utilityLlm.d.ts +14 -5
  91. package/dist/llm/utilityLlm.js +18 -13
  92. package/dist/plugin.adapter.d.ts +154 -6
  93. package/dist/plugin.adapter.js +53 -10
  94. package/dist/ports/auth-store.port.d.ts +73 -0
  95. package/dist/ports/auth-store.port.js +24 -0
  96. package/dist/ports/host-credentials.port.d.ts +22 -0
  97. package/dist/ports/host-credentials.port.js +4 -0
  98. package/dist/ports/model-call-completion.port.d.ts +9 -0
  99. package/dist/ports/model-call-completion.port.js +2 -0
  100. package/dist/ports/model-endpoints.port.d.ts +66 -19
  101. package/dist/ports/model-endpoints.port.js +22 -4
  102. package/dist/ports/model-retry-policy.port.d.ts +17 -0
  103. package/dist/ports/model-retry-policy.port.js +3 -0
  104. package/dist/ports/model-settings.port.d.ts +32 -0
  105. package/dist/ports/model-settings.port.js +4 -0
  106. package/dist/ports/models-transport.port.d.ts +53 -0
  107. package/dist/ports/models-transport.port.js +7 -0
  108. package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
  109. package/dist/ports/prompt-cache-frame.port.js +4 -0
  110. package/dist/ports/settings-store.port.d.ts +70 -0
  111. package/dist/ports/settings-store.port.js +33 -0
  112. package/dist/ports/thinking-binding.port.d.ts +7 -0
  113. package/dist/ports/thinking-binding.port.js +4 -0
  114. package/dist/settings/localSettings.d.ts +2 -1
  115. package/dist/settings/localSettings.js +18 -61
  116. package/dist/settings/memory-settings.adapter.d.ts +12 -0
  117. package/dist/settings/memory-settings.adapter.js +11 -0
  118. package/dist/settings/roleView.d.ts +3 -1
  119. package/dist/settings/roleView.js +1 -1
  120. package/dist/settings/settings-store.conformance.d.ts +47 -0
  121. package/dist/settings/settings-store.conformance.js +100 -0
  122. package/dist/settings/settings-value.entity.d.ts +15 -0
  123. package/dist/settings/settings-value.entity.functions.d.ts +51 -0
  124. package/dist/settings/settings-value.entity.functions.js +42 -0
  125. package/dist/settings/settings-value.entity.js +2 -0
  126. package/dist/storage/json-file.d.ts +18 -0
  127. package/dist/storage/json-file.js +26 -0
  128. package/dist/transport/http-proxy.entity.d.ts +104 -0
  129. package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
  130. package/dist/transport/http-proxy.entity.functions.js +112 -0
  131. package/dist/transport/http-proxy.entity.js +42 -0
  132. package/dist/transport/model-request.conformance.d.ts +13 -0
  133. package/dist/transport/model-request.conformance.js +36 -0
  134. package/dist/transport/models-transport.adapter.d.ts +49 -0
  135. package/dist/transport/models-transport.adapter.js +99 -0
  136. package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
  137. package/dist/transport/resilient-http-client.adapter.js +177 -0
  138. package/dist/transport/undici-fetch.adapter.d.ts +13 -0
  139. package/dist/transport/undici-fetch.adapter.js +1 -0
  140. package/package.json +13 -6
  141. package/dist/auth/anthropicOAuth.test.d.ts +0 -1
  142. package/dist/auth/localAuth.test.d.ts +0 -1
  143. package/dist/llm/compat.test.d.ts +0 -1
  144. package/dist/llm/modelCatalog.test.d.ts +0 -1
  145. package/dist/llm/providers.test.d.ts +0 -1
  146. package/dist/llm/retry.test.d.ts +0 -1
  147. package/dist/llm/router.test.d.ts +0 -1
  148. package/dist/llm/sse.test.d.ts +0 -1
  149. package/dist/plugin.adapter.test.d.ts +0 -1
  150. package/dist/settings/localSettings.test.d.ts +0 -1
  151. package/dist/settings/roleView.test.d.ts +0 -1
@@ -0,0 +1,73 @@
1
+ import { Option, Result, Schema } from "effect";
2
+ import { CachePosition, PromptCacheControl, PromptCacheError, PromptCacheSettings } from "./prompt-cache.entity.js";
3
+ const error = (reason, message) => new PromptCacheError({ reason, message }), lifetime = (control) => control.ttl ?? "5m", callerControl = (control) => {
4
+ if (control === null || control === void 0)
5
+ return Result.succeed(Option.none());
6
+ const invalid = () => Result.fail(error("Control", "A prompt cache control must be ephemeral with an optional five-minute or one-hour lifetime."));
7
+ if (typeof control !== "object")
8
+ return invalid();
9
+ const prototype = Object.getPrototypeOf(control);
10
+ if (prototype !== Object.prototype && prototype !== null)
11
+ return invalid();
12
+ if ("toJSON" in control || !Object.hasOwn(control, "type") || "ttl" in control && !Object.hasOwn(control, "ttl") || !Reflect.ownKeys(control).every((key) => {
13
+ const field = Object.getOwnPropertyDescriptor(control, key);
14
+ return (key === "type" || key === "ttl") && field !== void 0 && field.enumerable && Object.hasOwn(field, "value");
15
+ }))
16
+ return invalid();
17
+ if (!Schema.is(PromptCacheControl)(control) || Result.isFailure(Schema.decodeUnknownResult(PromptCacheControl)(control, { onExcessProperty: "error" })))
18
+ return invalid();
19
+ return Result.succeed(Option.some(control));
20
+ }, orderedLifetimes = (indexes, ttlAt) => {
21
+ const lifetimes = indexes.toSorted((left, right) => left - right).map(ttlAt), firstShort = lifetimes.indexOf("5m");
22
+ return firstShort === -1 || lifetimes.lastIndexOf("1h") < firstShort;
23
+ };
24
+ export const planBreakpoints = (input) => Result.gen(function* () {
25
+ const controls = yield* Result.all(input.positions.map((position) => CachePosition.match(position, {
26
+ Unmarked: () => Result.succeed(Option.none()),
27
+ Caller: (value) => callerControl(value.control),
28
+ Auto: () => Result.succeed(Option.none())
29
+ }))), callerTopLevel = yield* callerControl(input.callerTopLevel), callerIndexes = controls.flatMap((control, index) => Option.isSome(control) ? [index] : []);
30
+ if (!input.capability) {
31
+ if (callerIndexes.length > 0 || Option.isSome(callerTopLevel))
32
+ return yield* Result.fail(error("Capability", "This Messages endpoint does not support prompt cache controls."));
33
+ return { selectedAuto: [], topLevel: Option.none() };
34
+ }
35
+ const topLevel = Option.orElse(callerTopLevel, () => PromptCacheSettings.match(input.policy, {
36
+ Auto: () => Option.none(),
37
+ TopLevel: (value) => Option.some({ type: "ephemeral", ttl: value.ttl }),
38
+ Off: () => Option.none()
39
+ })), finalIndex = input.positions.length - 1;
40
+ if (Option.isSome(topLevel) && finalIndex < 0)
41
+ return yield* Result.fail(error("TopLevel", "A top-level prompt cache control needs a cacheable request block."));
42
+ const finalCaller = controls[finalIndex] ?? Option.none();
43
+ if (Option.isSome(topLevel) && Option.exists(finalCaller, (control) => lifetime(control) !== lifetime(topLevel.value)))
44
+ return yield* Result.fail(error("TopLevel", "The top-level prompt cache control must match the final block's lifetime."));
45
+ const occupied = Option.isSome(topLevel) && Option.isNone(finalCaller) ? [...callerIndexes, finalIndex] : callerIndexes;
46
+ if (occupied.length > 4)
47
+ return yield* Result.fail(error("Capacity", "A Messages request supports at most four effective prompt cache breakpoints."));
48
+ const autoTtl = PromptCacheSettings.match(input.policy, {
49
+ Auto: (value) => value.ttl,
50
+ TopLevel: (value) => value.ttl,
51
+ Off: () => "5m"
52
+ }), ttlAt = (index) => {
53
+ if (index === finalIndex && Option.isSome(topLevel))
54
+ return lifetime(topLevel.value);
55
+ return Option.match(controls[index] ?? Option.none(), { onNone: () => autoTtl, onSome: lifetime });
56
+ };
57
+ if (!orderedLifetimes(occupied, ttlAt))
58
+ return yield* Result.fail(error("TtlOrder", "Place one-hour prompt cache controls before five-minute controls."));
59
+ return { selectedAuto: PromptCacheSettings.match(input.policy, {
60
+ Auto: () => input.positions.flatMap((position, index) => CachePosition.match(position, {
61
+ Unmarked: () => [],
62
+ Caller: () => [],
63
+ Auto: (value) => [{ index, priority: value.priority }]
64
+ })).toSorted((left, right) => left.priority - right.priority || right.index - left.index).reduce((selected, candidate) => {
65
+ if (candidate.index === finalIndex && Option.isSome(topLevel))
66
+ return selected;
67
+ const next = [...occupied, ...selected, candidate.index];
68
+ return next.length <= 4 && orderedLifetimes(next, ttlAt) ? [...selected, candidate.index] : selected;
69
+ }, []),
70
+ TopLevel: () => [],
71
+ Off: () => []
72
+ }).toSorted((left, right) => left - right), topLevel };
73
+ });
@@ -0,0 +1,27 @@
1
+ import { Schema } from "effect";
2
+ export const PromptCacheTtl = Schema.Literals(["5m", "1h"]), PromptCacheSettings = Schema.Union([
3
+ Schema.TaggedStruct("Auto", { ttl: PromptCacheTtl }),
4
+ Schema.TaggedStruct("TopLevel", { ttl: PromptCacheTtl }),
5
+ Schema.TaggedStruct("Off", {})
6
+ ]).pipe(Schema.toTaggedUnion("_tag")), PromptCacheControl = Schema.Struct({
7
+ type: Schema.Literal("ephemeral"),
8
+ ttl: Schema.optionalKey(PromptCacheTtl)
9
+ }), CachePosition = Schema.Union([
10
+ Schema.TaggedStruct("Unmarked", {}),
11
+ Schema.TaggedStruct("Caller", { control: Schema.Unknown }),
12
+ Schema.TaggedStruct("Auto", { priority: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) })
13
+ ]).pipe(Schema.toTaggedUnion("_tag")), BreakpointInput = Schema.Struct({
14
+ positions: Schema.Array(CachePosition),
15
+ policy: PromptCacheSettings,
16
+ capability: Schema.Boolean,
17
+ callerTopLevel: Schema.optionalKey(Schema.Unknown)
18
+ }), BreakpointPlan = Schema.Struct({
19
+ selectedAuto: Schema.Array(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))).check(Schema.makeFilter((indexes) => indexes.every((index, offset) => offset === 0 || (indexes[offset - 1] ?? -1) < index) ? !0 : "automatic indexes must be strictly increasing")),
20
+ topLevel: Schema.OptionFromNullOr(PromptCacheControl)
21
+ });
22
+
23
+ export class PromptCacheError extends Schema.TaggedError()("PromptCacheError", {
24
+ reason: Schema.Literals(["Control", "Capability", "Capacity", "TtlOrder", "TopLevel"]),
25
+ message: Schema.String
26
+ }) {
27
+ }
@@ -0,0 +1,9 @@
1
+ import { AnthropicClient } from "@effect/ai-anthropic";
2
+ import { OpenAiClient } from "@effect/ai-openai";
3
+ import type { ProviderFailureProtocol } from "./provider-failure.entity.js";
4
+ /** Inspect decoded raw data before the SDK merges context overflow into a length cut. */
5
+ export declare const withAnthropicFailures: (client: AnthropicClient.Service) => AnthropicClient.Service;
6
+ /** The same raw error interception serves native Responses and its Codex transport. */
7
+ export declare const withOpenAiFailures: (client: OpenAiClient.Service) => OpenAiClient.Service;
8
+ /** The adapter protocol the provider classifier interprets, not the configured provider alias. */
9
+ export declare const providerFailureProtocol: (route: "anthropic" | "openai-responses" | "openai-codex" | "compat") => ProviderFailureProtocol;
@@ -0,0 +1,60 @@
1
+ import { AnthropicClient } from "@effect/ai-anthropic";
2
+ import { OpenAiClient } from "@effect/ai-openai";
3
+ import { Effect, Exit, Match, Option, Predicate, Schema, Stream } from "effect";
4
+ import { AiError } from "effect/ai";
5
+ import { modelFailureMessageOf, stampAiErrorFailure } from "@xandreed/core";
6
+ import { classifyProviderFailure } from "./provider-failure.entity.functions.js";
7
+ import { ProviderFailureEvidence } from "./provider-failure.entity.js";
8
+ import { completeSdkResponseAttempt, modelOutcomeOfExit, observeResponseUsage } from "./model-completion.adapter.js";
9
+ const encodeBody = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.Json)), jsonObject = Schema.decodeUnknownOption(Schema.JsonObject), emptyUsage = {}, ofUsage = (value) => Option.getOrElse(jsonObject(value), () => emptyUsage), httpDetails = (response, body) => {
10
+ const url = URL.canParse(response.request.url) ? new URL(response.request.url) : new URL("https://provider.invalid/"), headers = Object.fromEntries(Object.entries(response.headers).filter(([key]) => ["request-id", "x-request-id", "retry-after"].includes(key)));
11
+ return {
12
+ request: { method: "POST", url: `${url.protocol}//${url.host}${url.pathname}`, urlParams: [], headers: {} },
13
+ response: { status: response.status, headers },
14
+ body
15
+ };
16
+ }, nativeFailure = Effect.fnUntraced(function* (method, evidence, response, raw, requestId) {
17
+ const kind = Option.getOrElse(classifyProviderFailure(evidence), () => "Unknown"), body = yield* encodeBody(raw).pipe(Effect.orDie), http = httpDetails(response, body), metadata = evidence.protocol === "anthropic" ? { anthropic: { errorType: evidence.type ?? evidence.stopReason ?? null, requestId } } : { openai: { errorCode: evidence.code ?? null, errorType: evidence.type ?? null, requestId } }, description = modelFailureMessageOf(kind), reason = Match.value(kind).pipe(Match.when("Authentication", () => new AiError.AuthenticationError({ kind: "Unknown", description, metadata, http })), Match.when("Quota", () => new AiError.QuotaExhaustedError({ metadata, http })), Match.when("RateLimit", () => new AiError.RateLimitError({ metadata, http })), Match.when("InvalidResponse", () => new AiError.InvalidResponseError({ description, metadata, http })), Match.when("Transient", () => new AiError.InternalProviderError({ description, metadata, http })), Match.when("Timeout", () => new AiError.InternalProviderError({ description, metadata, http })), Match.when("Unknown", () => new AiError.UnknownError({ description, metadata, http })), Match.orElse(() => new AiError.InvalidRequestError({ description, metadata, http }))), error = stampAiErrorFailure(AiError.make({ module: "Models", method, reason }), kind);
18
+ yield* completeSdkResponseAttempt(response, { _tag: "Failed", failure: Option.some(kind) });
19
+ return error;
20
+ }), requestIdOf = (response) => response.headers["request-id"] ?? response.headers["x-request-id"] ?? null;
21
+ export const withAnthropicFailures = (client) => ({
22
+ ...client,
23
+ createMessage: (options) => client.createMessage(options).pipe(Effect.tap(([body, response]) => observeResponseUsage(response, body.usage, !0)), Effect.flatMap(([body, response]) => body.stop_reason === "model_context_window_exceeded" ? nativeFailure("createMessage", { protocol: "anthropic", status: response.status, stopReason: body.stop_reason }, response, body, requestIdOf(response)).pipe(Effect.flatMap(Effect.fail)) : completeSdkResponseAttempt(response, { _tag: "Completed" }).pipe(Effect.as([body, response])))),
24
+ createMessageStream: (options) => client.createMessageStream(options).pipe(Effect.map(([response, stream]) => [
25
+ response,
26
+ stream.pipe(Stream.mapAccumEffect(() => emptyUsage, (before, event) => Match.value(event).pipe(Match.when({ type: "error" }, (value) => nativeFailure("createMessageStream", {
27
+ protocol: "anthropic",
28
+ status: response.status,
29
+ type: value.error.type,
30
+ message: value.error.message
31
+ }, response, { event: value, usage: before }, value.request_id ?? requestIdOf(response)).pipe(Effect.flatMap(Effect.fail))), Match.when({ type: "message_start" }, (value) => {
32
+ const usage = ofUsage(value.message.usage);
33
+ return observeResponseUsage(response, value.message.usage, !1).pipe(Effect.andThen(value.message.stop_reason === "model_context_window_exceeded" ? nativeFailure("createMessageStream", { protocol: "anthropic", status: response.status, stopReason: value.message.stop_reason }, response, { event: value, usage }, requestIdOf(response)).pipe(Effect.flatMap(Effect.fail)) : Effect.succeed([usage, [event]])));
34
+ }), Match.when({ type: "message_delta" }, (value) => {
35
+ const usage = { ...before, ...Object.fromEntries(Object.entries(ofUsage(value.usage)).filter(([, count]) => count !== null)) };
36
+ return observeResponseUsage(response, value.usage, value.delta.stop_reason != null).pipe(Effect.andThen(value.delta.stop_reason === "model_context_window_exceeded" ? nativeFailure("createMessageStream", { protocol: "anthropic", status: response.status, stopReason: value.delta.stop_reason }, response, { event: value, usage }, requestIdOf(response)).pipe(Effect.flatMap(Effect.fail)) : Effect.succeed([usage, [event]])));
37
+ }), Match.when({ type: "message_stop" }, () => completeSdkResponseAttempt(response, { _tag: "Completed" }).pipe(Effect.as([before, [event]]))), Match.orElse(() => Effect.succeed([before, [event]])))), Stream.onEnd(completeSdkResponseAttempt(response, { _tag: "Completed" })), Stream.onExit((exit) => completeSdkResponseAttempt(response, Exit.isSuccess(exit) ? { _tag: "Interrupted" } : modelOutcomeOfExit(exit))))
38
+ ]))
39
+ });
40
+ const openAiFailure = (method, body, response, raw = body) => nativeFailure(method, {
41
+ protocol: "openai",
42
+ status: response.status,
43
+ ...body.error == null ? {} : { code: body.error.code, message: body.error.message }
44
+ }, response, raw, requestIdOf(response));
45
+ export const withOpenAiFailures = (client) => ({
46
+ ...client,
47
+ createResponse: (options) => client.createResponse(options).pipe(Effect.tap(([body, response]) => observeResponseUsage(response, body.usage, !0)), Effect.flatMap(([body, response]) => body.error != null ? openAiFailure("createResponse", body, response).pipe(Effect.flatMap(Effect.fail)) : completeSdkResponseAttempt(response, { _tag: "Completed" }).pipe(Effect.as([body, response])))),
48
+ createResponseStream: (options) => client.createResponseStream(options).pipe(Effect.map(([response, stream]) => [
49
+ response,
50
+ stream.pipe(Stream.mapAccumEffect(() => emptyUsage, (before, event) => {
51
+ const observed = Predicate.hasProperty(event, "response") && Predicate.hasProperty(event.response, "usage") ? ofUsage(event.response.usage) : emptyUsage, usage = { ...before, ...Object.fromEntries(Object.entries(observed).filter(([, count]) => count !== null)) }, raw = { event, usage }, final = event.type === "response.completed" || event.type === "response.failed" || event.type === "response.incomplete";
52
+ return observeResponseUsage(response, observed, final).pipe(Effect.andThen(Match.value(event).pipe(Match.when({ type: "error" }, (value) => nativeFailure("createResponseStream", {
53
+ protocol: "openai",
54
+ status: Schema.is(ProviderFailureEvidence.fields.status)(value.status) ? value.status : response.status,
55
+ ...value.code == null ? {} : { code: value.code },
56
+ message: value.message
57
+ }, response, raw, requestIdOf(response)).pipe(Effect.flatMap(Effect.fail))), Match.when({ type: "response.failed" }, (value) => openAiFailure("createResponseStream", value.response, response, raw).pipe(Effect.flatMap(Effect.fail))), Match.when({ response: { error: (error) => error != null } }, (value) => openAiFailure("createResponseStream", value.response, response, raw).pipe(Effect.flatMap(Effect.fail))), Match.orElse(() => (final ? completeSdkResponseAttempt(response, { _tag: "Completed" }) : Effect.void).pipe(Effect.as([usage, [event]]))))));
58
+ }), Stream.onEnd(completeSdkResponseAttempt(response, { _tag: "Completed" })), Stream.onExit((exit) => completeSdkResponseAttempt(response, Exit.isSuccess(exit) ? { _tag: "Interrupted" } : modelOutcomeOfExit(exit))))
59
+ ]))
60
+ }), providerFailureProtocol = (route) => route === "anthropic" ? "anthropic" : route === "compat" ? "compat" : "openai";
@@ -0,0 +1,42 @@
1
+ import { Schema } from "effect";
2
+ /** The existing provider dialect whose error vocabulary is being interpreted. */
3
+ export declare const ProviderFailureProtocol: Schema.Literals<readonly ["anthropic", "openai", "compat"]>;
4
+ export type ProviderFailureProtocol = typeof ProviderFailureProtocol.Type;
5
+ /** Raw provider evidence, before status and SDK normalization discard its meaning. */
6
+ export declare const ProviderFailureEvidence: Schema.Struct<{
7
+ readonly protocol: Schema.Literals<readonly ["anthropic", "openai", "compat"]>;
8
+ readonly status: Schema.optionalKey<Schema.Int>;
9
+ readonly code: Schema.optionalKey<Schema.String>;
10
+ readonly type: Schema.optionalKey<Schema.String>;
11
+ readonly message: Schema.optionalKey<Schema.String>;
12
+ readonly stopReason: Schema.optionalKey<Schema.String>;
13
+ }>;
14
+ export type ProviderFailureEvidence = typeof ProviderFailureEvidence.Type;
15
+ /** Only known structured error fields are read; arbitrary response data is not searched. */
16
+ export declare const ProviderErrorDetail: Schema.Struct<{
17
+ readonly code: Schema.optionalKey<Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Number]>>>;
18
+ readonly type: Schema.optionalKey<Schema.NullOr<Schema.String>>;
19
+ readonly message: Schema.optionalKey<Schema.NullOr<Schema.String>>;
20
+ }>;
21
+ export type ProviderErrorDetail = typeof ProviderErrorDetail.Type;
22
+ /** HTTP providers and sockets expose these two established error-body shapes. */
23
+ export declare const ProviderErrorBody: Schema.Struct<{
24
+ readonly code: Schema.optionalKey<Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Number]>>>;
25
+ readonly type: Schema.optionalKey<Schema.NullOr<Schema.String>>;
26
+ readonly message: Schema.optionalKey<Schema.NullOr<Schema.String>>;
27
+ readonly error: Schema.optionalKey<Schema.Union<readonly [Schema.Struct<{
28
+ readonly code: Schema.optionalKey<Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Number]>>>;
29
+ readonly type: Schema.optionalKey<Schema.NullOr<Schema.String>>;
30
+ readonly message: Schema.optionalKey<Schema.NullOr<Schema.String>>;
31
+ }>, Schema.String]>>;
32
+ }>;
33
+ export type ProviderErrorBody = typeof ProviderErrorBody.Type;
34
+ /** The pinned SDK's namespaced HTTP metadata and top-level socket metadata. */
35
+ export declare const ProviderErrorMetadata: Schema.Struct<{
36
+ readonly errorCode: Schema.optionalKey<Schema.NullOr<Schema.String>>;
37
+ readonly errorType: Schema.optionalKey<Schema.NullOr<Schema.String>>;
38
+ readonly code: Schema.optionalKey<Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Number]>>>;
39
+ readonly type: Schema.optionalKey<Schema.NullOr<Schema.String>>;
40
+ readonly message: Schema.optionalKey<Schema.NullOr<Schema.String>>;
41
+ }>;
42
+ export type ProviderErrorMetadata = typeof ProviderErrorMetadata.Type;
@@ -0,0 +1,8 @@
1
+ import { Option } from "effect";
2
+ import type { AiError } from "effect/ai";
3
+ import type { ModelFailureKind } from "@xandreed/core";
4
+ import type { ProviderFailureEvidence, ProviderFailureProtocol } from "./provider-failure.entity.js";
5
+ /** Interpret only supported provider evidence; absence is not a guessed failure kind. */
6
+ export declare const classifyProviderFailure: (evidence: ProviderFailureEvidence) => Option.Option<ModelFailureKind>;
7
+ /** Refine provider evidence without changing an error's concrete reason or unrelated fields. */
8
+ export declare const classifyAiError: (error: AiError.AiError, protocol: ProviderFailureProtocol) => AiError.AiError;
@@ -0,0 +1,67 @@
1
+ import { Option, Predicate, Schema } from "effect";
2
+ import { ENGINE_KEY, ModelFailureStamp, modelFailureOf, stampAiErrorFailure } from "@xandreed/core";
3
+ import { ProviderErrorBody, ProviderErrorMetadata } from "./provider-failure.entity.js";
4
+ import { QUOTA_ERRORS } from "./chat-wire.entity.functions.js";
5
+ const quotaNames = new Set([...QUOTA_ERRORS].map((name) => name.toLowerCase()).concat("insufficient_quota_error")), contextNames = new Set(["context_length_exceeded", "context_window_exceeded", "model_context_window_exceeded"]), authenticationNames = new Set(["authentication_error", "permission_error", "invalid_api_key", "invalid_api_key_error", "invalid_authentication"]), rateNames = new Set(["rate_limit_error", "rate_limit_exceeded", "rate_limit_exceeded_error"]), transientNames = new Set(["overloaded_error", "api_error", "server_error", "service_unavailable_error"]), timeoutNames = new Set(["request_timeout", "timeout_error"]), invalidNames = new Set(["modelerror", "not_found_error", "request_too_large", "request_too_large_error", "content_policy_violation", "content_policy_error"]), namedKinds = [
6
+ [contextNames, "ContextOverflow"],
7
+ [quotaNames, "Quota"],
8
+ [authenticationNames, "Authentication"],
9
+ [rateNames, "RateLimit"],
10
+ [transientNames, "Transient"],
11
+ [timeoutNames, "Timeout"],
12
+ [invalidNames, "InvalidRequest"]
13
+ ], namedFailure = (name) => Option.flatMap(Option.fromNullishOr(name), (value) => Option.map(Option.fromNullishOr(namedKinds.find(([names]) => names.has(value.toLowerCase()))), ([, kind]) => kind)), contextPatterns = {
14
+ anthropic: [/\b(?:input|prompt|request)\b.{0,80}\bexceeds? (?:the |this model's )?context window\b/i],
15
+ openai: [/\bmaximum context length is \d+ tokens\b/i, /\b(?:input|prompt|request)\b.{0,80}\bexceeds? (?:the |this model's )?context window\b/i],
16
+ compat: [/\bmaximum context length is \d+ tokens\b/i, /\b(?:input|prompt|request)\b.{0,80}\bexceeds? (?:the |this model's )?context window\b/i]
17
+ };
18
+ export const classifyProviderFailure = (evidence) => {
19
+ if (evidence.protocol === "anthropic" && evidence.stopReason === "model_context_window_exceeded")
20
+ return Option.some("ContextOverflow");
21
+ const specific = Option.orElse(namedFailure(evidence.code), () => namedFailure(evidence.type));
22
+ if (Option.isSome(specific))
23
+ return specific;
24
+ const message = evidence.message ?? "", contextMessage = contextPatterns[evidence.protocol].some((pattern) => pattern.test(message)) || evidence.protocol !== "openai" && evidence.status !== 413 && /\bprompt (?:is )?too long\b/i.test(message);
25
+ if ([evidence.code, evidence.type].some((name) => name?.toLowerCase() === "invalid_request_error"))
26
+ return Option.some(contextMessage ? "ContextOverflow" : "InvalidRequest");
27
+ if (/\bmodel .*not supported\b/i.test(message))
28
+ return Option.some("InvalidRequest");
29
+ if (evidence.status === 402)
30
+ return Option.some("Quota");
31
+ if (evidence.status === 401 || evidence.status === 403)
32
+ return Option.some("Authentication");
33
+ if ((evidence.status === void 0 || [200, 400, 413, 422].includes(evidence.status)) && contextMessage)
34
+ return Option.some("ContextOverflow");
35
+ if (evidence.status === 429)
36
+ return Option.some("RateLimit");
37
+ if (evidence.status === 408 || evidence.status === 504)
38
+ return Option.some("Timeout");
39
+ if (evidence.status !== void 0 && evidence.status >= 500)
40
+ return Option.some("Transient");
41
+ if (evidence.status !== void 0 && evidence.status >= 400)
42
+ return Option.some("InvalidRequest");
43
+ return Option.none();
44
+ }, classifyAiError = (error, protocol) => {
45
+ const reason = error.reason, metadata = "metadata" in reason ? reason.metadata : {}, engine = Object.hasOwn(metadata, ENGINE_KEY) ? metadata[ENGINE_KEY] : void 0;
46
+ if (Predicate.isObject(engine) && Object.hasOwn(engine, "failure") && Option.isSome(Schema.decodeUnknownOption(ModelFailureStamp)(engine)))
47
+ return error;
48
+ if (reason._tag === "InvalidOutputError" && (protocol === "anthropic" && error.module === "AnthropicClient" && ["createMessage", "createMessageStream"].includes(error.method) || protocol === "openai" && error.module === "OpenAiClient"))
49
+ return stampAiErrorFailure(error, "InvalidResponse");
50
+ if (Option.isNone(modelFailureOf(error)) || reason._tag === "NetworkError")
51
+ return error;
52
+ const http = "http" in reason ? reason.http : void 0, body = Option.flatMap(Option.fromNullishOr(http?.body), Schema.decodeUnknownOption(Schema.fromJsonString(ProviderErrorBody))), namespace = protocol === "compat" ? error.module : protocol, provider = Schema.decodeUnknownOption(ProviderErrorMetadata)(Object.hasOwn(metadata, namespace) ? metadata[namespace] : {}), socket = Schema.decodeUnknownOption(ProviderErrorMetadata)(metadata), details = [
53
+ ...Option.toArray(Option.map(body, (value) => typeof value.error === "string" ? { message: value.error } : value.error ?? value)),
54
+ ...Option.toArray(Option.map(provider, (value) => ({ ...value, ...value.errorCode == null ? {} : { code: value.errorCode }, ...value.errorType == null ? {} : { type: value.errorType } }))),
55
+ ...Option.toArray(socket)
56
+ ], field = (key) => Option.fromNullishOr(details.flatMap((detail) => {
57
+ const value = detail[key];
58
+ return typeof value === "string" ? [value] : [];
59
+ })[0]), evidence = {
60
+ protocol,
61
+ ...http?.response === void 0 ? {} : { status: http.response.status },
62
+ ...Option.match(field("code"), { onNone: () => ({}), onSome: (code) => ({ code }) }),
63
+ ...Option.match(field("type"), { onNone: () => ({}), onSome: (type) => ({ type }) }),
64
+ message: Option.getOrElse(field("message"), () => reason.message)
65
+ };
66
+ return Option.match(classifyProviderFailure(evidence), { onNone: () => error, onSome: (kind) => stampAiErrorFailure(error, kind) });
67
+ };
@@ -0,0 +1,20 @@
1
+ import { Schema } from "effect";
2
+ export const ProviderFailureProtocol = Schema.Literals(["anthropic", "openai", "compat"]), ProviderFailureEvidence = Schema.Struct({
3
+ protocol: ProviderFailureProtocol,
4
+ status: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 }))),
5
+ code: Schema.optionalKey(Schema.String),
6
+ type: Schema.optionalKey(Schema.String),
7
+ message: Schema.optionalKey(Schema.String),
8
+ stopReason: Schema.optionalKey(Schema.String)
9
+ }), ProviderErrorDetail = Schema.Struct({
10
+ code: Schema.optionalKey(Schema.NullOr(Schema.Union([Schema.String, Schema.Number]))),
11
+ type: Schema.optionalKey(Schema.NullOr(Schema.String)),
12
+ message: Schema.optionalKey(Schema.NullOr(Schema.String))
13
+ }), ProviderErrorBody = Schema.Struct({
14
+ error: Schema.optionalKey(Schema.Union([ProviderErrorDetail, Schema.String])),
15
+ ...ProviderErrorDetail.fields
16
+ }), ProviderErrorMetadata = Schema.Struct({
17
+ ...ProviderErrorDetail.fields,
18
+ errorCode: Schema.optionalKey(Schema.NullOr(Schema.String)),
19
+ errorType: Schema.optionalKey(Schema.NullOr(Schema.String))
20
+ });
@@ -0,0 +1,79 @@
1
+ import { Schema } from "effect";
2
+ import type { Option } from "effect";
3
+ import type { ModelLimits, ReasoningEffortType } from "@xandreed/core";
4
+ /** Documented native controls, or an explicitly labelled family policy for future IDs. */
5
+ export declare const AnthropicModelTraits: Schema.Struct<{
6
+ readonly source: Schema.Literals<readonly ["documented", "family-policy"]>;
7
+ readonly modes: Schema.$Array<Schema.Literals<readonly ["Adaptive", "Budget", "BetweenTools", "Off"]>>;
8
+ readonly defaultThinking: Schema.OptionFromNullOr<Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Adaptive", {
9
+ readonly display: Schema.optionalKey<Schema.Literals<readonly ["summarized", "omitted"]>>;
10
+ }>, Schema.TaggedStruct<"Budget", {
11
+ readonly budgetTokens: Schema.Int;
12
+ readonly display: Schema.optionalKey<Schema.Literals<readonly ["summarized", "omitted"]>>;
13
+ }>, Schema.TaggedStruct<"BetweenTools", {}>, Schema.TaggedStruct<"Off", {}>]>>;
14
+ readonly defaultEffort: Schema.OptionFromNullOr<Schema.Literals<readonly ["low", "medium", "high", "xhigh", "max"]>>;
15
+ readonly forcedTools: Schema.OptionFromNullOr<Schema.Boolean>;
16
+ readonly reducedEfforts: Schema.OptionFromNullOr<Schema.$Array<Schema.Literals<readonly ["low", "medium", "high", "xhigh", "max"]>>>;
17
+ }>;
18
+ export type AnthropicModelTraits = typeof AnthropicModelTraits.Type;
19
+ /** The providers the models plugin builds itself; any other is a configured endpoint. */
20
+ export declare const BuiltInProvider: Schema.Literals<readonly ["opencode", "openai-codex", "openai", "anthropic", "google"]>;
21
+ export type BuiltInProvider = typeof BuiltInProvider.Type;
22
+ /** The OpenAI Responses API, at `apiUrl` (the SDK's own by default). */
23
+ export interface ResponsesRoute {
24
+ readonly _tag: "openai-responses";
25
+ readonly apiUrl: Option.Option<string>;
26
+ /** Sent as the model's `strictJsonSchema` option; none: the SDK's default. */
27
+ readonly strictJsonSchema: Option.Option<boolean>;
28
+ /** Whose calls carry the call policy's effort: every model's, or only a model's that lists efforts (a non-reasoning model refuses one). */
29
+ readonly effort: "always" | "listed";
30
+ }
31
+ /** An OpenAI-compatible gateway's chat completions. */
32
+ export interface ChatRoute {
33
+ readonly _tag: "compat";
34
+ /** The client's name in its failures. */
35
+ readonly moduleName: string;
36
+ readonly chatUrl: string;
37
+ /** Standard chat-completions fields only (`CompatConfig.standardOnly`). */
38
+ readonly standardOnly: boolean;
39
+ /** Whether the reasoning tokens are read (`UsageReading`). */
40
+ readonly usage: {
41
+ readonly reasoning: boolean;
42
+ };
43
+ }
44
+ /**
45
+ * The wire a built-in model is sent over (its `_tag`), with what its client
46
+ * needs beyond the model and the key: Anthropic's Messages API, the OpenAI
47
+ * Responses API, the subscription's Codex dialect of it, or a gateway's chat
48
+ * completions.
49
+ */
50
+ export type Route = {
51
+ readonly _tag: "anthropic";
52
+ } | ResponsesRoute | {
53
+ readonly _tag: "openai-codex";
54
+ } | ChatRoute;
55
+ /** A model of a built-in provider: whether its route sends it images (where not, they go as label lines), the efforts it takes, and whether its thinking is bound to the conversation. */
56
+ export interface ModelProfile {
57
+ readonly id: string;
58
+ readonly images: boolean;
59
+ readonly efforts: ReadonlyArray<ReasoningEffortType>;
60
+ readonly limits?: ModelLimits;
61
+ readonly anthropic?: AnthropicModelTraits;
62
+ /**
63
+ * The provider binds each thinking block's signature to the conversation
64
+ * before it (system, tools, earlier messages): a block sent back after that
65
+ * prefix changed is refused, unless the request says what to do with it
66
+ * (the models plugin's `thinkingPrefixMismatch`).
67
+ */
68
+ readonly bindsThinking: boolean;
69
+ }
70
+ /** One built-in provider: its catalog, what each model reads and takes, and the route each is sent over. */
71
+ export interface ProviderProfile {
72
+ /** The catalog's name for the provider's route, before ` · <model>`; none: its entries have no label. */
73
+ readonly label: Option.Option<string>;
74
+ /** The catalog: the models the provider lists, in order. */
75
+ readonly models: ReadonlyArray<ModelProfile>;
76
+ /** A model's profile, listed or not: one the catalog does not list follows its family. */
77
+ readonly modelOf: (modelId: string) => ModelProfile;
78
+ readonly routeOf: (modelId: string) => Route;
79
+ }
@@ -0,0 +1,23 @@
1
+ import { Option } from "effect";
2
+ import { BuiltInProvider } from "./provider-profile.entity.js";
3
+ import type { ModelProfile, ProviderProfile } from "./provider-profile.entity.js";
4
+ /**
5
+ * The built-in providers, one profile each. The catalog, the image and
6
+ * effort flags and the route builders read this table instead of naming a
7
+ * provider.
8
+ */
9
+ export declare const OPENCODE_CHAT_URL = "https://opencode.ai/zen/go/v1/chat/completions";
10
+ /** OpenCode serves GPT models through the OpenAI Responses protocol. */
11
+ export declare const OPENCODE_RESPONSES_API_URL = "https://opencode.ai/zen/v1";
12
+ /** Gemini through its OpenAI-compatible chat completions (Bearer API key). */
13
+ export declare const GOOGLE_CHAT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
14
+ /** Model-ID traits work for a configured Messages gateway as well as the builtin provider. */
15
+ export declare const anthropicProfileOf: (modelId: string) => ModelProfile;
16
+ /** Every built-in provider's profile, in the catalog's order of providers. */
17
+ export declare const PROVIDER_PROFILES: {
18
+ readonly [P in BuiltInProvider]: ProviderProfile;
19
+ };
20
+ /** A provider's profile; none for a provider that is not built in. */
21
+ export declare const profileOf: (provider: string) => Option.Option<ProviderProfile>;
22
+ /** A `"<provider>:<modelId>"` selection's model profile; none when it does not parse or its provider is not built in. */
23
+ export declare const modelProfileOf: (selection: string) => Option.Option<ModelProfile>;
@@ -0,0 +1,83 @@
1
+ import { Option, Schema } from "effect";
2
+ import { parseModelSelection } from "@xandreed/core";
3
+ import { BuiltInProvider } from "./provider-profile.entity.js";
4
+ export const OPENCODE_CHAT_URL = "https://opencode.ai/zen/go/v1/chat/completions", OPENCODE_RESPONSES_API_URL = "https://opencode.ai/zen/v1", GOOGLE_CHAT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
5
+ const OPENCODE_RESPONSES_MODEL = /^gpt-/i, TEXT_ONLY_CODEX = new Set(["gpt-5.3-codex-spark"]), PUBLIC_EFFORTS = ["low", "medium", "high"], SUBSCRIPTION_EFFORTS = ["low", "medium", "high", "xhigh"], ALL_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"], ANTHROPIC = { _tag: "anthropic" }, OPENAI_CODEX = { _tag: "openai-codex" }, OPENAI_RESPONSES = { _tag: "openai-responses", apiUrl: Option.none(), strictJsonSchema: Option.none(), effort: "listed" }, OPENCODE_RESPONSES = { _tag: "openai-responses", apiUrl: Option.some(OPENCODE_RESPONSES_API_URL), strictJsonSchema: Option.some(!1), effort: "always" }, OPENCODE_CHAT = { _tag: "compat", moduleName: "OpenCode", chatUrl: OPENCODE_CHAT_URL, standardOnly: !1, usage: { reasoning: !0 } }, GOOGLE_CHAT = { _tag: "compat", moduleName: "Google", chatUrl: GOOGLE_CHAT_URL, standardOnly: !0, usage: { reasoning: !1 } }, bindsThinkingToPrefix = (id) => {
6
+ const version = /^claude-(fable|mythos|opus|sonnet|haiku)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/.exec(id);
7
+ if (version === null)
8
+ return !1;
9
+ const [, family, major, minor] = version;
10
+ return Number(major) * 100 + Number(minor ?? 0) >= (family === "fable" || family === "mythos" ? 501 : 505);
11
+ }, NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"], advertised = (contextWindow, maxOutputTokens = 128000) => ({ contextWindow, maxOutputTokens }), adaptive = { _tag: "Adaptive", display: "summarized" }, off = { _tag: "Off" }, controls = (modes, defaultThinking, defaultEffort, forcedTools, reducedEfforts = Option.none()) => ({ source: "documented", modes, defaultThinking, defaultEffort, forcedTools, reducedEfforts }), knownAnthropic = {
12
+ "claude-fable-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: advertised(1e6), anthropic: controls(["Adaptive"], Option.some(adaptive), Option.some("high"), Option.some(!0)) },
13
+ "claude-fable-5-1": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !0, limits: advertised(1e6), anthropic: controls(["Adaptive"], Option.some(adaptive), Option.some("high"), Option.some(!1)) },
14
+ "claude-mythos-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: advertised(1e6), anthropic: controls(["Adaptive"], Option.some(adaptive), Option.some("high"), Option.some(!0)) },
15
+ "claude-mythos-5-1": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !0, limits: advertised(1e6), anthropic: controls(["Adaptive"], Option.some(adaptive), Option.some("high"), Option.some(!1)) },
16
+ "claude-opus-5-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !0, limits: advertised(1e6), anthropic: controls(["Adaptive"], Option.some(adaptive), Option.some("medium"), Option.some(!1)) },
17
+ "claude-sonnet-5-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !0, limits: advertised(1e6), anthropic: controls(["Adaptive", "BetweenTools"], Option.some(adaptive), Option.some("high"), Option.some(!1), Option.some(["low", "medium", "high"])) },
18
+ "claude-haiku-5-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !0, limits: advertised(1e6), anthropic: controls(["Adaptive", "Off"], Option.some(adaptive), Option.some("medium"), Option.some(!0), Option.some(["low", "medium", "high"])) },
19
+ "claude-opus-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: { maxOutputTokens: 128000 }, anthropic: controls(["Adaptive", "Off"], Option.some(adaptive), Option.some("high"), Option.some(!0), Option.some(["low", "medium", "high"])) },
20
+ "claude-sonnet-5": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: advertised(1e6), anthropic: controls(["Adaptive", "Off"], Option.some(adaptive), Option.some("high"), Option.some(!0)) },
21
+ "claude-opus-4-8": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: advertised(1e6), anthropic: controls(["Adaptive", "Off"], Option.some(off), Option.some("high"), Option.some(!0)) },
22
+ "claude-opus-4-7": { images: !0, efforts: NATIVE_EFFORTS, bindsThinking: !1, limits: { maxOutputTokens: 128000 }, anthropic: controls(["Adaptive", "Off"], Option.some(off), Option.some("high"), Option.some(!0)) },
23
+ "claude-opus-4-6": { images: !0, efforts: ["low", "medium", "high", "max"], bindsThinking: !1, limits: advertised(1e6), anthropic: controls(["Adaptive", "Budget", "Off"], Option.some(off), Option.some("high"), Option.some(!0)) },
24
+ "claude-sonnet-4-6": { images: !0, efforts: ["low", "medium", "high", "max"], bindsThinking: !1, limits: { maxOutputTokens: 128000 }, anthropic: controls(["Adaptive", "Budget", "Off"], Option.some(off), Option.some("high"), Option.some(!0)) },
25
+ "claude-opus-4-5": { images: !0, efforts: ["low", "medium", "high"], bindsThinking: !1, limits: { maxOutputTokens: 64000 }, anthropic: controls(["Budget", "Off"], Option.some(off), Option.some("high"), Option.some(!0)) },
26
+ "claude-sonnet-4-5": { images: !0, efforts: [], bindsThinking: !1, limits: { maxOutputTokens: 64000 }, anthropic: controls(["Budget", "Off"], Option.some(off), Option.none(), Option.some(!0)) },
27
+ "claude-haiku-4-5": { images: !0, efforts: [], bindsThinking: !1, limits: advertised(200000, 64000), anthropic: controls(["Budget", "Off"], Option.some(off), Option.none(), Option.some(!0)) }
28
+ };
29
+ export const anthropicProfileOf = (modelId) => {
30
+ const base = modelId.replace(/-\d{8}$/, ""), known = Object.hasOwn(knownAnthropic, base) ? knownAnthropic[base] : void 0;
31
+ if (known !== void 0)
32
+ return { id: modelId, ...known };
33
+ if (/^claude-(?:fable|mythos)-5(?:-\d{1,2})?$/.test(base))
34
+ return {
35
+ id: modelId,
36
+ images: !0,
37
+ efforts: [],
38
+ bindsThinking: bindsThinkingToPrefix(modelId),
39
+ anthropic: {
40
+ source: "family-policy",
41
+ modes: ["Adaptive"],
42
+ defaultThinking: Option.some(adaptive),
43
+ defaultEffort: Option.none(),
44
+ forcedTools: Option.none(),
45
+ reducedEfforts: Option.none()
46
+ }
47
+ };
48
+ return { id: modelId, images: !0, efforts: [], bindsThinking: bindsThinkingToPrefix(modelId) };
49
+ };
50
+ const profile = (label, ids, traits, routeOf) => {
51
+ const modelOf = (id) => ({ id, ...traits(id) });
52
+ return { label, models: ids.map(modelOf), modelOf, routeOf };
53
+ };
54
+ export const PROVIDER_PROFILES = {
55
+ opencode: profile(Option.none(), [
56
+ "glm-5.2",
57
+ "glm-5.1",
58
+ "kimi-k2.7-code",
59
+ "kimi-k2.6",
60
+ "mimo-v2.5-pro",
61
+ "mimo-v2.5",
62
+ "qwen3.7-max",
63
+ "qwen3.7-plus",
64
+ "qwen3.6-plus",
65
+ "minimax-m3",
66
+ "minimax-m2.7",
67
+ "deepseek-v4-pro",
68
+ "deepseek-v4-flash"
69
+ ], (id) => ({ images: OPENCODE_RESPONSES_MODEL.test(id), efforts: [], bindsThinking: !1 }), (id) => OPENCODE_RESPONSES_MODEL.test(id) ? OPENCODE_RESPONSES : OPENCODE_CHAT),
70
+ "openai-codex": profile(Option.some("OpenAI subscription"), ["gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"], (id) => ({
71
+ images: !TEXT_ONLY_CODEX.has(id),
72
+ efforts: /^gpt-5\.6-(luna|sol|terra)$/.test(id) ? ALL_EFFORTS : /^gpt-5/.test(id) ? SUBSCRIPTION_EFFORTS : [],
73
+ bindsThinking: !1
74
+ }), () => OPENAI_CODEX),
75
+ openai: profile(Option.some("OpenAI API key"), ["gpt-5", "gpt-5.1", "gpt-5.2", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"], (id) => ({ images: !0, efforts: /^gpt-5/.test(id) ? PUBLIC_EFFORTS : [], bindsThinking: !1 }), () => OPENAI_RESPONSES),
76
+ anthropic: profile(Option.none(), ["claude-fable-5", "claude-haiku-4-5", "claude-opus-4-8", "claude-sonnet-5", "claude-fable-5-1", "claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5"], (id) => {
77
+ const { id: _id, ...traits } = anthropicProfileOf(id);
78
+ return traits;
79
+ }, () => ANTHROPIC),
80
+ google: profile(Option.none(), ["gemini-3-flash", "gemini-3.1-pro", "gemini-3.5-flash"], () => ({ images: !0, efforts: [], bindsThinking: !1 }), () => GOOGLE_CHAT)
81
+ };
82
+ const isBuiltIn = Schema.is(BuiltInProvider);
83
+ export const profileOf = (provider) => isBuiltIn(provider) ? Option.some(PROVIDER_PROFILES[provider]) : Option.none(), modelProfileOf = (selection) => Option.flatMap(parseModelSelection(selection), ({ provider, modelId }) => Option.map(profileOf(provider), (found) => found.modelOf(modelId)));
@@ -0,0 +1,10 @@
1
+ import { Schema } from "effect";
2
+ import { AnthropicEffort, ModelThinking } from "./model-settings.entity.js";
3
+ export const AnthropicModelTraits = Schema.Struct({
4
+ source: Schema.Literals(["documented", "family-policy"]),
5
+ modes: Schema.Array(Schema.Literals(ModelThinking.discriminants)),
6
+ defaultThinking: Schema.OptionFromNullOr(ModelThinking),
7
+ defaultEffort: Schema.OptionFromNullOr(AnthropicEffort),
8
+ forcedTools: Schema.OptionFromNullOr(Schema.Boolean),
9
+ reducedEfforts: Schema.OptionFromNullOr(Schema.Array(AnthropicEffort))
10
+ }), BuiltInProvider = Schema.Literals(["opencode", "openai-codex", "openai", "anthropic", "google"]);