rax-flow-providers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/claude-adapter.d.ts +41 -0
  2. package/dist/claude-adapter.d.ts.map +1 -0
  3. package/dist/claude-adapter.js +236 -0
  4. package/dist/claude-adapter.js.map +1 -0
  5. package/dist/cohere-adapter.d.ts +37 -0
  6. package/dist/cohere-adapter.d.ts.map +1 -0
  7. package/dist/cohere-adapter.js +160 -0
  8. package/dist/cohere-adapter.js.map +1 -0
  9. package/dist/error-mapper.d.ts +51 -0
  10. package/dist/error-mapper.d.ts.map +1 -0
  11. package/dist/error-mapper.js +132 -0
  12. package/dist/error-mapper.js.map +1 -0
  13. package/dist/gemini-adapter.d.ts +37 -0
  14. package/dist/gemini-adapter.d.ts.map +1 -0
  15. package/dist/gemini-adapter.js +150 -0
  16. package/dist/gemini-adapter.js.map +1 -0
  17. package/dist/groq-adapter.d.ts +35 -0
  18. package/dist/groq-adapter.d.ts.map +1 -0
  19. package/dist/groq-adapter.js +152 -0
  20. package/dist/groq-adapter.js.map +1 -0
  21. package/dist/host-bridge-adapter.d.ts +20 -0
  22. package/dist/host-bridge-adapter.d.ts.map +1 -0
  23. package/dist/host-bridge-adapter.js +145 -0
  24. package/dist/host-bridge-adapter.js.map +1 -0
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mistral-adapter.d.ts +39 -0
  30. package/dist/mistral-adapter.d.ts.map +1 -0
  31. package/dist/mistral-adapter.js +172 -0
  32. package/dist/mistral-adapter.js.map +1 -0
  33. package/dist/openai-adapter.d.ts +30 -0
  34. package/dist/openai-adapter.d.ts.map +1 -0
  35. package/dist/openai-adapter.js +171 -0
  36. package/dist/openai-adapter.js.map +1 -0
  37. package/dist/pricing.d.ts +15 -0
  38. package/dist/pricing.d.ts.map +1 -0
  39. package/dist/pricing.js +61 -0
  40. package/dist/pricing.js.map +1 -0
  41. package/dist/rest-adapter.d.ts +32 -0
  42. package/dist/rest-adapter.d.ts.map +1 -0
  43. package/dist/rest-adapter.js +124 -0
  44. package/dist/rest-adapter.js.map +1 -0
  45. package/dist/strategy.d.ts +38 -0
  46. package/dist/strategy.d.ts.map +1 -0
  47. package/dist/strategy.js +117 -0
  48. package/dist/strategy.js.map +1 -0
  49. package/dist/utils.d.ts +3 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +22 -0
  52. package/dist/utils.js.map +1 -0
  53. package/package.json +18 -0
  54. package/src/claude-adapter.ts +350 -0
  55. package/src/cohere-adapter.ts +262 -0
  56. package/src/error-mapper.ts +187 -0
  57. package/src/gemini-adapter.ts +246 -0
  58. package/src/groq-adapter.ts +234 -0
  59. package/src/host-bridge-adapter.ts +189 -0
  60. package/src/index.ts +11 -0
  61. package/src/mistral-adapter.ts +262 -0
  62. package/src/openai-adapter.ts +240 -0
  63. package/src/pricing.ts +77 -0
  64. package/src/rest-adapter.ts +181 -0
  65. package/src/strategy.ts +166 -0
  66. package/src/utils.ts +18 -0
  67. package/tsconfig.json +18 -0
package/src/pricing.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file pricing.ts
3
+ * Token pricing catalog and cost calculation.
4
+ *
5
+ * Prices are in USD per 1M tokens unless specified.
6
+ * Updated: 2026-02 (simulated current date)
7
+ */
8
+
9
+ interface ModelPrice {
10
+ input: number; // $ per 1M tokens
11
+ output: number; // $ per 1M tokens
12
+ }
13
+
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // Official Pricing Catalog (approximate / standard rates)
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ const PRICING_CATALOG: Record<string, ModelPrice> = {
19
+ // OpenAI
20
+ "gpt-4o": { input: 2.50, output: 10.00 },
21
+ "gpt-4o-2024-05-13": { input: 2.50, output: 10.00 },
22
+ "gpt-4o-mini": { input: 0.15, output: 0.60 },
23
+ "gpt-4.5-preview": { input: 75.00, output: 150.00 }, // hypothetical high-end
24
+ "o1": { input: 15.00, output: 60.00 },
25
+ "o3-mini": { input: 1.10, output: 4.40 },
26
+
27
+ // Anthropic
28
+ "claude-3-5-sonnet-latest": { input: 3.00, output: 15.00 },
29
+ "claude-3-5-haiku-latest": { input: 0.25, output: 1.25 },
30
+ "claude-3-opus-20240229": { input: 15.00, output: 75.00 },
31
+
32
+ // Google (Gemini 1.5 / 2.0)
33
+ "gemini-2.0-flash": { input: 0.10, output: 0.40 },
34
+ "gemini-1.5-flash": { input: 0.075, output: 0.30 },
35
+ "gemini-1.5-pro": { input: 1.25, output: 5.00 },
36
+
37
+ // Mistral
38
+ "mistral-large-latest": { input: 2.00, output: 6.00 },
39
+ "mistral-small-latest": { input: 0.20, output: 0.60 },
40
+ "codestral-latest": { input: 1.00, output: 3.00 },
41
+ "open-mixtral-8x22b": { input: 2.00, output: 6.00 },
42
+
43
+ // Groq (Llama 3 hosted)
44
+ "llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
45
+ "llama3-8b-8192": { input: 0.05, output: 0.08 },
46
+ "mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
47
+
48
+ // Cohere
49
+ "command-r-plus-08-2024": { input: 2.50, output: 10.00 },
50
+ "command-r-08-2024": { input: 0.15, output: 0.60 },
51
+ };
52
+
53
+ // Default fallback price (cheap generic model assumption)
54
+ const DEFAULT_PRICE: ModelPrice = { input: 0.50, output: 1.50 };
55
+
56
+ /** Returns the price config for a given model ID (with fuzzy matching). */
57
+ function getPriceConfig(model: string): ModelPrice {
58
+ if (PRICING_CATALOG[model]) return PRICING_CATALOG[model];
59
+
60
+ // Fuzzy match for versioned models (e.g. gpt-4o-2024-08-06 -> gpt-4o)
61
+ const base = Object.keys(PRICING_CATALOG).find((k) => model.startsWith(k));
62
+ return base ? PRICING_CATALOG[base] : DEFAULT_PRICE;
63
+ }
64
+
65
+ /**
66
+ * Calculates the estimated cost in USD for a given usage.
67
+ */
68
+ export function calculateCost(
69
+ model: string,
70
+ usage?: { promptTokens: number; completionTokens: number }
71
+ ): number {
72
+ if (!usage) return 0;
73
+ const price = getPriceConfig(model);
74
+ const inputCost = (usage.promptTokens / 1_000_000) * price.input;
75
+ const outputCost = (usage.completionTokens / 1_000_000) * price.output;
76
+ return inputCost + outputCost;
77
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @file rest-adapter.ts
3
+ * Generic REST bridge adapter.
4
+ *
5
+ * Targets any OpenAI-compatible or custom LLM endpoint that accepts a simple
6
+ * JSON body with { model, prompt, temperature, maxTokens } and returns
7
+ * { output | content | text }.
8
+ *
9
+ * Useful for: LM Studio, local Ollama proxy, custom inference servers, etc.
10
+ */
11
+
12
+ import { IModelProvider, ModelResponse, ProviderCallOptions } from "@rax-flow/core";
13
+ import { parseJsonObjectFromText } from "./utils.js";
14
+ import { mapHttpError, mapNetworkError, mapParseError } from "./error-mapper.js";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Wire shapes
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ interface RestResponse {
21
+ output?: unknown;
22
+ content?: string;
23
+ text?: string;
24
+ error?: unknown;
25
+ }
26
+
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ // Adapter
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ const VENDOR = "rest";
32
+
33
+ export interface GenericRestAdapterOptions {
34
+ endpoint: string;
35
+ token?: string;
36
+ defaultModel?: string;
37
+ timeoutMs?: number;
38
+ }
39
+
40
+ export class GenericRestAdapter implements IModelProvider {
41
+ private readonly endpoint: string;
42
+ private readonly token?: string;
43
+ private readonly defaultModel: string;
44
+ private readonly timeoutMs: number;
45
+
46
+ constructor(options: GenericRestAdapterOptions);
47
+ /** @deprecated Prefer the options-object constructor. */
48
+ constructor(endpoint: string, token?: string);
49
+ constructor(
50
+ optionsOrEndpoint: GenericRestAdapterOptions | string,
51
+ legacyToken?: string
52
+ ) {
53
+ if (typeof optionsOrEndpoint === "string") {
54
+ this.endpoint = optionsOrEndpoint;
55
+ this.token = legacyToken;
56
+ this.defaultModel = "generic-llm";
57
+ this.timeoutMs = 30_000;
58
+ } else {
59
+ this.endpoint = optionsOrEndpoint.endpoint;
60
+ this.token = optionsOrEndpoint.token;
61
+ this.defaultModel = optionsOrEndpoint.defaultModel ?? "generic-llm";
62
+ this.timeoutMs = optionsOrEndpoint.timeoutMs ?? 30_000;
63
+ }
64
+ }
65
+
66
+ // ── private helpers ────────────────────────────────────────────────────────
67
+
68
+ private async post(
69
+ body: Record<string, unknown>,
70
+ context: "callModel" | "callStructured"
71
+ ): Promise<RestResponse> {
72
+ const controller = new AbortController();
73
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
74
+
75
+ let res: Response;
76
+ try {
77
+ res = await fetch(this.endpoint, {
78
+ method: "POST",
79
+ headers: {
80
+ "content-type": "application/json",
81
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
82
+ },
83
+ body: JSON.stringify(body),
84
+ signal: controller.signal,
85
+ });
86
+ } catch (err) {
87
+ clearTimeout(timer);
88
+ throw mapNetworkError(VENDOR, err);
89
+ } finally {
90
+ clearTimeout(timer);
91
+ }
92
+
93
+ const raw = await res.json().catch(() => ({}));
94
+
95
+ if (!res.ok) {
96
+ throw mapHttpError(VENDOR, res.status, raw, context);
97
+ }
98
+
99
+ return raw as RestResponse;
100
+ }
101
+
102
+ private extractText(payload: RestResponse): string {
103
+ if (typeof payload.output === "string") return payload.output;
104
+ if (payload.content) return payload.content;
105
+ if (payload.text) return payload.text;
106
+ return JSON.stringify(payload.output ?? payload);
107
+ }
108
+
109
+ // ── IModelProvider ─────────────────────────────────────────────────────────
110
+
111
+ async callModel(
112
+ prompt: string,
113
+ options?: ProviderCallOptions
114
+ ): Promise<ModelResponse<string>> {
115
+ const started = Date.now();
116
+ const model = options?.model ?? this.defaultModel;
117
+
118
+ const payload = await this.post(
119
+ {
120
+ model,
121
+ prompt,
122
+ temperature: options?.temperature ?? 0.2,
123
+ maxTokens: options?.maxTokens ?? 1200,
124
+ },
125
+ "callModel"
126
+ );
127
+
128
+ return {
129
+ provider: VENDOR,
130
+ model,
131
+ latencyMs: Date.now() - started,
132
+ output: this.extractText(payload),
133
+ raw: payload,
134
+ };
135
+ }
136
+
137
+ async callStructured<T>(
138
+ prompt: string,
139
+ schema: object,
140
+ options?: ProviderCallOptions
141
+ ): Promise<ModelResponse<T>> {
142
+ const started = Date.now();
143
+ const model = options?.model ?? this.defaultModel;
144
+
145
+ const augmentedPrompt = [
146
+ prompt,
147
+ "",
148
+ "Return ONLY valid JSON matching this schema:",
149
+ JSON.stringify(schema),
150
+ ].join("\n");
151
+
152
+ const payload = await this.post(
153
+ {
154
+ model,
155
+ prompt: augmentedPrompt,
156
+ temperature: 0,
157
+ maxTokens: options?.maxTokens ?? 1400,
158
+ },
159
+ "callStructured"
160
+ );
161
+
162
+ const text = this.extractText(payload);
163
+ const parsed = parseJsonObjectFromText<T>(text);
164
+
165
+ if (!parsed) {
166
+ throw mapParseError(VENDOR, "callStructured", text);
167
+ }
168
+
169
+ return {
170
+ provider: VENDOR,
171
+ model,
172
+ latencyMs: Date.now() - started,
173
+ output: parsed,
174
+ raw: payload,
175
+ };
176
+ }
177
+
178
+ async healthCheck(): Promise<boolean> {
179
+ return Boolean(this.endpoint);
180
+ }
181
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @file strategy.ts
3
+ * Provider selection strategies: smart fallback, retries, aggregation.
4
+ *
5
+ * Uses `RaxProviderError` codes to decide whether to retry the same provider
6
+ * or immediately move to the next one in the fallback chain.
7
+ */
8
+
9
+ import { IModelProvider, ModelResponse, ProviderCallOptions } from "@rax-flow/core";
10
+ import { RaxProviderError, RETRYABLE_CODES } from "./error-mapper.js";
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Types
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ export interface FallbackCallOptions {
17
+ /** How many times to retry a retryable error on the SAME provider before moving on. */
18
+ maxRetriesPerProvider?: number;
19
+ /** Delay in ms between retries (simple linear backoff). */
20
+ retryDelayMs?: number;
21
+ /** Per-call options forwarded to each provider. */
22
+ callOptions?: ProviderCallOptions;
23
+ }
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Smart fallback
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Tries each provider in order.
31
+ * - If a `RaxProviderError` with `isRetryable=true` is thrown, retries up to
32
+ * `maxRetriesPerProvider` times (with linear back-off) before moving on.
33
+ * - If a `RaxProviderError` with `shouldFallback=true` is thrown, skips to
34
+ * the next provider immediately.
35
+ * - Any other error is also treated as a skip trigger.
36
+ */
37
+ export async function fallbackCall(
38
+ providers: IModelProvider[],
39
+ prompt: string,
40
+ options: FallbackCallOptions = {}
41
+ ): Promise<ModelResponse<string>> {
42
+ const { maxRetriesPerProvider = 1, retryDelayMs = 500, callOptions } = options;
43
+
44
+ const errors: string[] = [];
45
+
46
+ for (const provider of providers) {
47
+ const healthy = await provider.healthCheck().catch(() => false);
48
+ if (!healthy) continue;
49
+
50
+ let attempt = 0;
51
+ let lastError: unknown;
52
+
53
+ while (attempt <= maxRetriesPerProvider) {
54
+ try {
55
+ return await provider.callModel(prompt, callOptions);
56
+ } catch (err) {
57
+ lastError = err;
58
+
59
+ if (err instanceof RaxProviderError) {
60
+ if (err.isRetryable && attempt < maxRetriesPerProvider) {
61
+ // Retry this provider after a delay
62
+ await sleep(retryDelayMs * (attempt + 1));
63
+ attempt++;
64
+ continue;
65
+ }
66
+ // Non-retryable or retries exhausted → next provider
67
+ errors.push(err.message);
68
+ break;
69
+ }
70
+
71
+ // Unknown error → move on immediately
72
+ errors.push(err instanceof Error ? err.message : String(err));
73
+ break;
74
+ }
75
+ }
76
+
77
+ if (lastError) {
78
+ // Log for observability but continue to next provider
79
+ continue;
80
+ }
81
+ }
82
+
83
+ throw new Error(`fallback_chain_exhausted: all providers failed.\n${errors.join("\n")}`);
84
+ }
85
+
86
+ /**
87
+ * Structured-output variant of the fallback chain.
88
+ */
89
+ export async function fallbackCallStructured<T>(
90
+ providers: IModelProvider[],
91
+ prompt: string,
92
+ schema: object,
93
+ options: FallbackCallOptions = {}
94
+ ): Promise<ModelResponse<T>> {
95
+ const { maxRetriesPerProvider = 1, retryDelayMs = 500, callOptions } = options;
96
+
97
+ const errors: string[] = [];
98
+
99
+ for (const provider of providers) {
100
+ const healthy = await provider.healthCheck().catch(() => false);
101
+ if (!healthy) continue;
102
+
103
+ let attempt = 0;
104
+
105
+ while (attempt <= maxRetriesPerProvider) {
106
+ try {
107
+ return await provider.callStructured<T>(prompt, schema, callOptions);
108
+ } catch (err) {
109
+ if (err instanceof RaxProviderError && err.isRetryable && attempt < maxRetriesPerProvider) {
110
+ await sleep(retryDelayMs * (attempt + 1));
111
+ attempt++;
112
+ continue;
113
+ }
114
+ errors.push(err instanceof Error ? err.message : String(err));
115
+ break;
116
+ }
117
+ }
118
+ }
119
+
120
+ throw new Error(
121
+ `fallback_structured_chain_exhausted: all providers failed.\n${errors.join("\n")}`
122
+ );
123
+ }
124
+
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+ // Aggregate / consensus
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Calls all providers in parallel and picks the longest non-empty output
131
+ * as a simple consensus heuristic. Useful for ensemble verification.
132
+ */
133
+ export async function aggregateCalls(
134
+ providers: IModelProvider[],
135
+ prompt: string,
136
+ callOptions?: ProviderCallOptions
137
+ ): Promise<{ outputs: ModelResponse<string>[]; consensus: string }> {
138
+ const outputs = await Promise.all(
139
+ providers.map(async (p) => {
140
+ try {
141
+ return await p.callModel(prompt, callOptions);
142
+ } catch {
143
+ return {
144
+ provider: "unknown",
145
+ model: "failed",
146
+ latencyMs: 0,
147
+ output: "",
148
+ } as ModelResponse<string>;
149
+ }
150
+ })
151
+ );
152
+
153
+ const nonEmpty = outputs.filter((o: ModelResponse<string>) => o.output.length > 0);
154
+ const consensus =
155
+ nonEmpty.sort((a: ModelResponse<string>, b: ModelResponse<string>) => b.output.length - a.output.length)[0]?.output ?? "";
156
+
157
+ return { outputs, consensus };
158
+ }
159
+
160
+ // ─────────────────────────────────────────────────────────────────────────────
161
+ // Internal helpers
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+
164
+ function sleep(ms: number): Promise<void> {
165
+ return new Promise((resolve) => setTimeout(resolve, ms));
166
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function parseJsonObjectFromText<T>(text: string): T | null {
2
+ try {
3
+ return JSON.parse(text) as T;
4
+ } catch {
5
+ const match = text.match(/\{[\s\S]*\}/);
6
+ if (!match) return null;
7
+ try {
8
+ return JSON.parse(match[0]) as T;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ }
14
+
15
+ export function asString(value: unknown): string {
16
+ if (typeof value === "string") return value;
17
+ return JSON.stringify(value);
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true,
7
+ "declaration": true,
8
+ "declarationMap": true
9
+ },
10
+ "include": [
11
+ "src/**/*.ts"
12
+ ],
13
+ "references": [
14
+ {
15
+ "path": "../core"
16
+ }
17
+ ]
18
+ }