memhook 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/bin/memhook.d.ts +16 -0
- package/dist/bin/memhook.d.ts.map +1 -0
- package/dist/bin/memhook.js +122 -0
- package/dist/bin/memhook.js.map +1 -0
- package/dist/src/cache.d.ts +30 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +80 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/catalog.d.ts +20 -0
- package/dist/src/catalog.d.ts.map +1 -0
- package/dist/src/catalog.js +152 -0
- package/dist/src/catalog.js.map +1 -0
- package/dist/src/config.d.ts +60 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +172 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/configFile.d.ts +54 -0
- package/dist/src/configFile.d.ts.map +1 -0
- package/dist/src/configFile.js +51 -0
- package/dist/src/configFile.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/preFilter.d.ts +16 -0
- package/dist/src/preFilter.d.ts.map +1 -0
- package/dist/src/preFilter.js +40 -0
- package/dist/src/preFilter.js.map +1 -0
- package/dist/src/providers/anthropic.d.ts +33 -0
- package/dist/src/providers/anthropic.d.ts.map +1 -0
- package/dist/src/providers/anthropic.js +98 -0
- package/dist/src/providers/anthropic.js.map +1 -0
- package/dist/src/providers/factory.d.ts +15 -0
- package/dist/src/providers/factory.d.ts.map +1 -0
- package/dist/src/providers/factory.js +37 -0
- package/dist/src/providers/factory.js.map +1 -0
- package/dist/src/providers/http.d.ts +34 -0
- package/dist/src/providers/http.d.ts.map +1 -0
- package/dist/src/providers/http.js +60 -0
- package/dist/src/providers/http.js.map +1 -0
- package/dist/src/providers/ollama.d.ts +30 -0
- package/dist/src/providers/ollama.d.ts.map +1 -0
- package/dist/src/providers/ollama.js +89 -0
- package/dist/src/providers/ollama.js.map +1 -0
- package/dist/src/providers/openai.d.ts +31 -0
- package/dist/src/providers/openai.d.ts.map +1 -0
- package/dist/src/providers/openai.js +94 -0
- package/dist/src/providers/openai.js.map +1 -0
- package/dist/src/providers/types.d.ts +48 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +18 -0
- package/dist/src/providers/types.js.map +1 -0
- package/dist/src/router.d.ts +32 -0
- package/dist/src/router.d.ts.map +1 -0
- package/dist/src/router.js +342 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/version.d.ts +13 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +13 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic provider — Haiku 4.5 default for memhook.
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* POST https://api.anthropic.com/v1/messages
|
|
6
|
+
* Headers: x-api-key, anthropic-version
|
|
7
|
+
* Body: { model, max_tokens, system: [{type, text, cache_control}], messages }
|
|
8
|
+
*
|
|
9
|
+
* Anthropic-specific concepts (ephemeral prompt caching, `anthropic-beta`
|
|
10
|
+
* headers) are passed via `AnthropicProviderOptions` at construction time, NOT
|
|
11
|
+
* through the shared `SelectionRequest` — so the OpenAI and Ollama adapters
|
|
12
|
+
* never see them. Cache control TTL "1h" is GA in 2026; no beta header is
|
|
13
|
+
* required, but a non-empty `betaHeaders` list still maps to `anthropic-beta`
|
|
14
|
+
* for forward-compat.
|
|
15
|
+
*
|
|
16
|
+
* Retry: single retry on 429/503 with 500ms backoff (max 2 attempts), via the
|
|
17
|
+
* shared `postJsonWithRetry` transport.
|
|
18
|
+
*/
|
|
19
|
+
import type { Provider, ProviderConfig, SelectionRequest, SelectionResponse } from "./types.js";
|
|
20
|
+
/** Anthropic-only knobs, kept off the shared provider interface. */
|
|
21
|
+
export interface AnthropicProviderOptions {
|
|
22
|
+
betaHeaders?: string[];
|
|
23
|
+
cacheControlTtl?: "5m" | "1h";
|
|
24
|
+
}
|
|
25
|
+
export declare class AnthropicProvider implements Provider {
|
|
26
|
+
private readonly config;
|
|
27
|
+
private readonly options;
|
|
28
|
+
readonly name = "anthropic";
|
|
29
|
+
private readonly apiKey;
|
|
30
|
+
constructor(config: ProviderConfig, options?: AnthropicProviderOptions);
|
|
31
|
+
select(req: SelectionRequest): Promise<SelectionResponse>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAGpB,oEAAoE;AACpE,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC/B;AAOD,qBAAa,iBAAkB,YAAW,QAAQ;IAK9C,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,QAAQ,CAAC,IAAI,eAAe;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAGb,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,wBAA6B;IAOnD,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAuChE"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic provider — Haiku 4.5 default for memhook.
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* POST https://api.anthropic.com/v1/messages
|
|
6
|
+
* Headers: x-api-key, anthropic-version
|
|
7
|
+
* Body: { model, max_tokens, system: [{type, text, cache_control}], messages }
|
|
8
|
+
*
|
|
9
|
+
* Anthropic-specific concepts (ephemeral prompt caching, `anthropic-beta`
|
|
10
|
+
* headers) are passed via `AnthropicProviderOptions` at construction time, NOT
|
|
11
|
+
* through the shared `SelectionRequest` — so the OpenAI and Ollama adapters
|
|
12
|
+
* never see them. Cache control TTL "1h" is GA in 2026; no beta header is
|
|
13
|
+
* required, but a non-empty `betaHeaders` list still maps to `anthropic-beta`
|
|
14
|
+
* for forward-compat.
|
|
15
|
+
*
|
|
16
|
+
* Retry: single retry on 429/503 with 500ms backoff (max 2 attempts), via the
|
|
17
|
+
* shared `postJsonWithRetry` transport.
|
|
18
|
+
*/
|
|
19
|
+
import { postJsonWithRetry } from "./http.js";
|
|
20
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
21
|
+
const DEFAULT_BASE_URL = "https://api.anthropic.com/v1/messages";
|
|
22
|
+
const RETRY_BACKOFF_MS = 500;
|
|
23
|
+
const RETRY_STATUSES = [429, 503];
|
|
24
|
+
export class AnthropicProvider {
|
|
25
|
+
config;
|
|
26
|
+
options;
|
|
27
|
+
name = "anthropic";
|
|
28
|
+
apiKey;
|
|
29
|
+
constructor(config, options = {}) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.options = options;
|
|
32
|
+
if (!config.apiKey)
|
|
33
|
+
throw new Error("AnthropicProvider: apiKey is required");
|
|
34
|
+
if (!config.model)
|
|
35
|
+
throw new Error("AnthropicProvider: model is required");
|
|
36
|
+
this.apiKey = config.apiKey;
|
|
37
|
+
}
|
|
38
|
+
async select(req) {
|
|
39
|
+
const ttl = this.options.cacheControlTtl;
|
|
40
|
+
const body = JSON.stringify({
|
|
41
|
+
model: this.config.model,
|
|
42
|
+
max_tokens: req.maxOutputTokens,
|
|
43
|
+
system: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: req.systemPrompt,
|
|
47
|
+
cache_control: ttl ? { type: "ephemeral", ttl } : { type: "ephemeral" },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
messages: [{ role: "user", content: req.userPrompt }],
|
|
51
|
+
});
|
|
52
|
+
const headers = {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"x-api-key": this.apiKey,
|
|
55
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
56
|
+
};
|
|
57
|
+
const betas = this.options.betaHeaders ?? [];
|
|
58
|
+
if (betas.length > 0)
|
|
59
|
+
headers["anthropic-beta"] = betas.join(",");
|
|
60
|
+
const { json, httpStatus, latencyMs } = await postJsonWithRetry({
|
|
61
|
+
url: this.config.baseUrl ?? DEFAULT_BASE_URL,
|
|
62
|
+
headers,
|
|
63
|
+
body,
|
|
64
|
+
timeoutMs: req.timeoutMs,
|
|
65
|
+
retryStatuses: RETRY_STATUSES,
|
|
66
|
+
backoffMs: RETRY_BACKOFF_MS,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
rawText: extractText(json),
|
|
70
|
+
usage: extractUsage(json),
|
|
71
|
+
latencyMs,
|
|
72
|
+
httpStatus,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function extractText(json) {
|
|
77
|
+
if (!json)
|
|
78
|
+
return "";
|
|
79
|
+
const content = json["content"];
|
|
80
|
+
if (!Array.isArray(content))
|
|
81
|
+
return "";
|
|
82
|
+
const first = content[0];
|
|
83
|
+
return typeof first?.text === "string" ? first.text : "";
|
|
84
|
+
}
|
|
85
|
+
function extractUsage(json) {
|
|
86
|
+
const usage = json?.["usage"] ?? {};
|
|
87
|
+
const num = (key) => {
|
|
88
|
+
const v = usage[key];
|
|
89
|
+
return typeof v === "number" ? v : 0;
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
inputTokens: num("input_tokens"),
|
|
93
|
+
outputTokens: num("output_tokens"),
|
|
94
|
+
cacheCreateTokens: num("cache_creation_input_tokens"),
|
|
95
|
+
cacheReadTokens: num("cache_read_input_tokens"),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/providers/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider factory — builds the configured provider from `MemhookConfig`.
|
|
3
|
+
*
|
|
4
|
+
* The router imports only this factory, never the concrete adapter classes, so
|
|
5
|
+
* provider selection lives in exactly one place. The `never` default arm gives
|
|
6
|
+
* a compile error if a new `provider.type` union member is added without a
|
|
7
|
+
* matching case here.
|
|
8
|
+
*
|
|
9
|
+
* Construction may throw (e.g. a required field missing) — the router wraps the
|
|
10
|
+
* call in try/catch and falls back to empty `additionalContext` (fail-soft).
|
|
11
|
+
*/
|
|
12
|
+
import type { Provider } from "./types.js";
|
|
13
|
+
import type { MemhookConfig } from "../config.js";
|
|
14
|
+
export declare function createProvider(cfg: MemhookConfig, apiKey: string | undefined): Provider;
|
|
15
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/providers/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAsBvF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider factory — builds the configured provider from `MemhookConfig`.
|
|
3
|
+
*
|
|
4
|
+
* The router imports only this factory, never the concrete adapter classes, so
|
|
5
|
+
* provider selection lives in exactly one place. The `never` default arm gives
|
|
6
|
+
* a compile error if a new `provider.type` union member is added without a
|
|
7
|
+
* matching case here.
|
|
8
|
+
*
|
|
9
|
+
* Construction may throw (e.g. a required field missing) — the router wraps the
|
|
10
|
+
* call in try/catch and falls back to empty `additionalContext` (fail-soft).
|
|
11
|
+
*/
|
|
12
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
13
|
+
import { OpenAIProvider } from "./openai.js";
|
|
14
|
+
import { OllamaProvider } from "./ollama.js";
|
|
15
|
+
export function createProvider(cfg, apiKey) {
|
|
16
|
+
const base = {
|
|
17
|
+
model: cfg.provider.model,
|
|
18
|
+
...(apiKey !== undefined && { apiKey }),
|
|
19
|
+
...(cfg.provider.baseUrl !== undefined && { baseUrl: cfg.provider.baseUrl }),
|
|
20
|
+
};
|
|
21
|
+
switch (cfg.provider.type) {
|
|
22
|
+
case "anthropic":
|
|
23
|
+
return new AnthropicProvider(base, {
|
|
24
|
+
betaHeaders: cfg.provider.betaHeaders,
|
|
25
|
+
cacheControlTtl: cfg.selection.cacheControlTtl,
|
|
26
|
+
});
|
|
27
|
+
case "openai":
|
|
28
|
+
return new OpenAIProvider(base);
|
|
29
|
+
case "ollama":
|
|
30
|
+
return new OllamaProvider(base);
|
|
31
|
+
default: {
|
|
32
|
+
const exhaustive = cfg.provider.type;
|
|
33
|
+
throw new Error(`createProvider: unknown provider type ${String(exhaustive)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/providers/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,UAAU,cAAc,CAAC,GAAkB,EAAE,MAA0B;IAC3E,MAAM,IAAI,GAAmB;QAC3B,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK;QACzB,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC;QACvC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;KAC7E,CAAC;IAEF,QAAQ,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC1B,KAAK,WAAW;YACd,OAAO,IAAI,iBAAiB,CAAC,IAAI,EAAE;gBACjC,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,WAAW;gBACrC,eAAe,EAAE,GAAG,CAAC,SAAS,CAAC,eAAe;aAC/C,CAAC,CAAC;QACL,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;QAClC,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,UAAU,GAAU,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC5C,MAAM,IAAI,KAAK,CAAC,yCAAyC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP transport for providers — one audited network path.
|
|
3
|
+
*
|
|
4
|
+
* Every provider POSTs JSON and wants the same behaviour: an AbortController
|
|
5
|
+
* timeout, a single retry on a transient failure (network error or a
|
|
6
|
+
* retryable status), latency measurement, and a tolerant JSON parse that
|
|
7
|
+
* never throws on a malformed body. Centralising it keeps the fail-soft
|
|
8
|
+
* contract verifiable in one place (failsoft-auditor rules 4 + 7) instead of
|
|
9
|
+
* being re-implemented per adapter.
|
|
10
|
+
*
|
|
11
|
+
* Semantics (identical to memhook v0.1's Anthropic path):
|
|
12
|
+
* - max 2 attempts.
|
|
13
|
+
* - attempt 1 throws (network error) -> backoff, retry.
|
|
14
|
+
* - attempt 1 returns a retryable status -> backoff, retry.
|
|
15
|
+
* - attempt 2 throws -> rethrow (caller's try/catch -> fail-soft).
|
|
16
|
+
* - otherwise return the parsed body (or null if the body wasn't JSON).
|
|
17
|
+
*/
|
|
18
|
+
export interface PostJsonOptions {
|
|
19
|
+
url: string;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
body: string;
|
|
22
|
+
timeoutMs: number;
|
|
23
|
+
/** HTTP statuses worth a single retry (e.g. 429, 503). */
|
|
24
|
+
retryStatuses: readonly number[];
|
|
25
|
+
backoffMs: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RawHttpResult {
|
|
28
|
+
/** Parsed JSON body, or null when the body was absent/not valid JSON. */
|
|
29
|
+
json: Record<string, unknown> | null;
|
|
30
|
+
httpStatus: number;
|
|
31
|
+
latencyMs: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function postJsonWithRetry(opts: PostJsonOptions): Promise<RawHttpResult>;
|
|
34
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/providers/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAuCrF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP transport for providers — one audited network path.
|
|
3
|
+
*
|
|
4
|
+
* Every provider POSTs JSON and wants the same behaviour: an AbortController
|
|
5
|
+
* timeout, a single retry on a transient failure (network error or a
|
|
6
|
+
* retryable status), latency measurement, and a tolerant JSON parse that
|
|
7
|
+
* never throws on a malformed body. Centralising it keeps the fail-soft
|
|
8
|
+
* contract verifiable in one place (failsoft-auditor rules 4 + 7) instead of
|
|
9
|
+
* being re-implemented per adapter.
|
|
10
|
+
*
|
|
11
|
+
* Semantics (identical to memhook v0.1's Anthropic path):
|
|
12
|
+
* - max 2 attempts.
|
|
13
|
+
* - attempt 1 throws (network error) -> backoff, retry.
|
|
14
|
+
* - attempt 1 returns a retryable status -> backoff, retry.
|
|
15
|
+
* - attempt 2 throws -> rethrow (caller's try/catch -> fail-soft).
|
|
16
|
+
* - otherwise return the parsed body (or null if the body wasn't JSON).
|
|
17
|
+
*/
|
|
18
|
+
export async function postJsonWithRetry(opts) {
|
|
19
|
+
let last = null;
|
|
20
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
21
|
+
const started = Date.now();
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
|
|
24
|
+
let resp;
|
|
25
|
+
try {
|
|
26
|
+
resp = await fetch(opts.url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: opts.headers,
|
|
29
|
+
body: opts.body,
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
if (attempt === 1) {
|
|
36
|
+
await sleep(opts.backoffMs);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
const latencyMs = Date.now() - started;
|
|
43
|
+
const status = resp.status;
|
|
44
|
+
if (attempt === 1 && opts.retryStatuses.includes(status)) {
|
|
45
|
+
await sleep(opts.backoffMs);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const json = (await resp.json().catch(() => null));
|
|
49
|
+
last = { json, httpStatus: status, latencyMs };
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (!last) {
|
|
53
|
+
throw new Error("postJsonWithRetry: no response after retries");
|
|
54
|
+
}
|
|
55
|
+
return last;
|
|
56
|
+
}
|
|
57
|
+
function sleep(ms) {
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/providers/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAmBH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAqB;IAC3D,IAAI,IAAI,GAAyB,IAAI,CAAC;IACtC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,IAAc,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;gBAClB,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBAC5B,SAAS;YACX,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAE3B,IAAI,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC5B,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAmC,CAAC;QACrF,IAAI,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/C,MAAM;IACR,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama provider — local models, `llama3.1` default.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (native chat endpoint, NOT the OpenAI-compat layer):
|
|
5
|
+
* POST http://localhost:11434/api/chat
|
|
6
|
+
* No Authorization header (local Ollama needs no key).
|
|
7
|
+
* Body: { model, messages, stream:false, format:"json", options:{temperature,num_predict} }
|
|
8
|
+
*
|
|
9
|
+
* The native `/api/chat` is preferred over `/v1/chat/completions` because it
|
|
10
|
+
* exposes deterministic `options` first-class, returns a flat
|
|
11
|
+
* `message.content` + top-level token counts (matching memhook's response
|
|
12
|
+
* shape), and requires no dummy `api_key` the OpenAI-compat clients demand.
|
|
13
|
+
* `stream:false` forces a single JSON body (otherwise the response is
|
|
14
|
+
* newline-delimited chunks that would break parsing). `format:"json"` nudges
|
|
15
|
+
* weaker local models toward parseable output; the router's `extractJsonArray`
|
|
16
|
+
* still pulls the basename array out tolerantly.
|
|
17
|
+
*
|
|
18
|
+
* Fail-soft notes: a missing model (404 `model 'x' not found`) or a stopped
|
|
19
|
+
* daemon (ECONNREFUSED) surfaces as empty `rawText` / a thrown fetch, both of
|
|
20
|
+
* which the router treats as an empty selection. Cold model load can be slow,
|
|
21
|
+
* so the config layer gives Ollama a more generous default timeout.
|
|
22
|
+
*/
|
|
23
|
+
import type { Provider, ProviderConfig, SelectionRequest, SelectionResponse } from "./types.js";
|
|
24
|
+
export declare class OllamaProvider implements Provider {
|
|
25
|
+
private readonly config;
|
|
26
|
+
readonly name = "ollama";
|
|
27
|
+
constructor(config: ProviderConfig);
|
|
28
|
+
select(req: SelectionRequest): Promise<SelectionResponse>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ollama.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.d.ts","sourceRoot":"","sources":["../../../src/providers/ollama.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,qBAAa,cAAe,YAAW,QAAQ;IAGjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,YAAY;gBAEI,MAAM,EAAE,cAAc;IAI7C,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAmChE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama provider — local models, `llama3.1` default.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (native chat endpoint, NOT the OpenAI-compat layer):
|
|
5
|
+
* POST http://localhost:11434/api/chat
|
|
6
|
+
* No Authorization header (local Ollama needs no key).
|
|
7
|
+
* Body: { model, messages, stream:false, format:"json", options:{temperature,num_predict} }
|
|
8
|
+
*
|
|
9
|
+
* The native `/api/chat` is preferred over `/v1/chat/completions` because it
|
|
10
|
+
* exposes deterministic `options` first-class, returns a flat
|
|
11
|
+
* `message.content` + top-level token counts (matching memhook's response
|
|
12
|
+
* shape), and requires no dummy `api_key` the OpenAI-compat clients demand.
|
|
13
|
+
* `stream:false` forces a single JSON body (otherwise the response is
|
|
14
|
+
* newline-delimited chunks that would break parsing). `format:"json"` nudges
|
|
15
|
+
* weaker local models toward parseable output; the router's `extractJsonArray`
|
|
16
|
+
* still pulls the basename array out tolerantly.
|
|
17
|
+
*
|
|
18
|
+
* Fail-soft notes: a missing model (404 `model 'x' not found`) or a stopped
|
|
19
|
+
* daemon (ECONNREFUSED) surfaces as empty `rawText` / a thrown fetch, both of
|
|
20
|
+
* which the router treats as an empty selection. Cold model load can be slow,
|
|
21
|
+
* so the config layer gives Ollama a more generous default timeout.
|
|
22
|
+
*/
|
|
23
|
+
import { postJsonWithRetry } from "./http.js";
|
|
24
|
+
const DEFAULT_BASE_URL = "http://localhost:11434/api/chat";
|
|
25
|
+
const RETRY_BACKOFF_MS = 500;
|
|
26
|
+
// No HTTP status is worth retrying for a local daemon (404 model-not-found /
|
|
27
|
+
// 400 won't fix on retry); the shared transport still retries once on a thrown
|
|
28
|
+
// network error, covering a transient connection blip.
|
|
29
|
+
const RETRY_STATUSES = [];
|
|
30
|
+
export class OllamaProvider {
|
|
31
|
+
config;
|
|
32
|
+
name = "ollama";
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
if (!config.model)
|
|
36
|
+
throw new Error("OllamaProvider: model is required");
|
|
37
|
+
}
|
|
38
|
+
async select(req) {
|
|
39
|
+
const body = JSON.stringify({
|
|
40
|
+
model: this.config.model,
|
|
41
|
+
messages: [
|
|
42
|
+
{ role: "system", content: req.systemPrompt },
|
|
43
|
+
{ role: "user", content: req.userPrompt },
|
|
44
|
+
],
|
|
45
|
+
stream: false,
|
|
46
|
+
format: "json",
|
|
47
|
+
options: {
|
|
48
|
+
temperature: 0,
|
|
49
|
+
num_predict: req.maxOutputTokens,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const headers = {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
};
|
|
55
|
+
const { json, httpStatus, latencyMs } = await postJsonWithRetry({
|
|
56
|
+
url: this.config.baseUrl ?? DEFAULT_BASE_URL,
|
|
57
|
+
headers,
|
|
58
|
+
body,
|
|
59
|
+
timeoutMs: req.timeoutMs,
|
|
60
|
+
retryStatuses: RETRY_STATUSES,
|
|
61
|
+
backoffMs: RETRY_BACKOFF_MS,
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
rawText: extractText(json),
|
|
65
|
+
usage: extractUsage(json),
|
|
66
|
+
latencyMs,
|
|
67
|
+
httpStatus,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function extractText(json) {
|
|
72
|
+
if (!json)
|
|
73
|
+
return "";
|
|
74
|
+
const message = json["message"];
|
|
75
|
+
return typeof message?.content === "string" ? message.content : "";
|
|
76
|
+
}
|
|
77
|
+
function extractUsage(json) {
|
|
78
|
+
const num = (key) => {
|
|
79
|
+
const v = json?.[key];
|
|
80
|
+
return typeof v === "number" ? v : 0;
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
inputTokens: num("prompt_eval_count"),
|
|
84
|
+
outputTokens: num("eval_count"),
|
|
85
|
+
cacheCreateTokens: 0,
|
|
86
|
+
cacheReadTokens: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../../src/providers/ollama.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,gBAAgB,GAAG,iCAAiC,CAAC;AAC3D,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,6EAA6E;AAC7E,+EAA+E;AAC/E,uDAAuD;AACvD,MAAM,cAAc,GAAG,EAAW,CAAC;AAEnC,MAAM,OAAO,cAAc;IAGI;IAFpB,IAAI,GAAG,QAAQ,CAAC;IAEzB,YAA6B,MAAsB;QAAtB,WAAM,GAAN,MAAM,CAAgB;QACjD,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAqB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;YACxB,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,YAAY,EAAE;gBAC7C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,UAAU,EAAE;aAC1C;YACD,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,WAAW,EAAE,CAAC;gBACd,WAAW,EAAE,GAAG,CAAC,eAAe;aACjC;SACF,CAAC,CAAC;QAEH,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QAEF,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,iBAAiB,CAAC;YAC9D,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,gBAAgB;YAC5C,OAAO;YACP,IAAI;YACJ,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,aAAa,EAAE,cAAc;YAC7B,SAAS,EAAE,gBAAgB;SAC5B,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC;YAC1B,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC;YACzB,SAAS;YACT,UAAU;SACX,CAAC;IACJ,CAAC;CACF;AAED,SAAS,WAAW,CAAC,IAAoC;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAsC,CAAC;IACrE,OAAO,OAAO,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,SAAS,YAAY,CAAC,IAAoC;IACxD,MAAM,GAAG,GAAG,CAAC,GAAW,EAAU,EAAE;QAClC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC;IACF,OAAO;QACL,WAAW,EAAE,GAAG,CAAC,mBAAmB,CAAC;QACrC,YAAY,EAAE,GAAG,CAAC,YAAY,CAAC;QAC/B,iBAAiB,EAAE,CAAC;QACpB,eAAe,EAAE,CAAC;KACnB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI provider — `gpt-4o-mini` default.
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* POST https://api.openai.com/v1/chat/completions
|
|
6
|
+
* Headers: Authorization: Bearer <key>
|
|
7
|
+
* Body: { model, messages: [{role:system},{role:user}], max_tokens, temperature }
|
|
8
|
+
*
|
|
9
|
+
* The static catalog is sent as the leading `system` message and the variable
|
|
10
|
+
* user prompt last, so OpenAI's automatic prompt caching (exact-prefix match,
|
|
11
|
+
* ≥1024 tokens) can engage on a large catalog with no per-request flag.
|
|
12
|
+
*
|
|
13
|
+
* We deliberately do NOT set `response_format`: the router's `extractJsonArray`
|
|
14
|
+
* already tolerantly pulls the basename array out of the raw text (and the
|
|
15
|
+
* shared system prompt asks for a bare JSON array, which JSON-object mode would
|
|
16
|
+
* fight). This keeps the adapter symmetric with Anthropic/Ollama — every
|
|
17
|
+
* provider returns `rawText` and the router owns parsing. `gpt-4o-mini` is a
|
|
18
|
+
* non-reasoning model, so plain `max_tokens` + `temperature: 0` apply (reasoning
|
|
19
|
+
* models would require `max_completion_tokens` and reject `temperature`).
|
|
20
|
+
*
|
|
21
|
+
* Retry: single retry on 429/5xx with 500ms backoff, via the shared transport.
|
|
22
|
+
*/
|
|
23
|
+
import type { Provider, ProviderConfig, SelectionRequest, SelectionResponse } from "./types.js";
|
|
24
|
+
export declare class OpenAIProvider implements Provider {
|
|
25
|
+
private readonly config;
|
|
26
|
+
readonly name = "openai";
|
|
27
|
+
private readonly apiKey;
|
|
28
|
+
constructor(config: ProviderConfig);
|
|
29
|
+
select(req: SelectionRequest): Promise<SelectionResponse>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/providers/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAOpB,qBAAa,cAAe,YAAW,QAAQ;IAIjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEH,MAAM,EAAE,cAAc;IAM7C,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAgChE"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI provider — `gpt-4o-mini` default.
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* POST https://api.openai.com/v1/chat/completions
|
|
6
|
+
* Headers: Authorization: Bearer <key>
|
|
7
|
+
* Body: { model, messages: [{role:system},{role:user}], max_tokens, temperature }
|
|
8
|
+
*
|
|
9
|
+
* The static catalog is sent as the leading `system` message and the variable
|
|
10
|
+
* user prompt last, so OpenAI's automatic prompt caching (exact-prefix match,
|
|
11
|
+
* ≥1024 tokens) can engage on a large catalog with no per-request flag.
|
|
12
|
+
*
|
|
13
|
+
* We deliberately do NOT set `response_format`: the router's `extractJsonArray`
|
|
14
|
+
* already tolerantly pulls the basename array out of the raw text (and the
|
|
15
|
+
* shared system prompt asks for a bare JSON array, which JSON-object mode would
|
|
16
|
+
* fight). This keeps the adapter symmetric with Anthropic/Ollama — every
|
|
17
|
+
* provider returns `rawText` and the router owns parsing. `gpt-4o-mini` is a
|
|
18
|
+
* non-reasoning model, so plain `max_tokens` + `temperature: 0` apply (reasoning
|
|
19
|
+
* models would require `max_completion_tokens` and reject `temperature`).
|
|
20
|
+
*
|
|
21
|
+
* Retry: single retry on 429/5xx with 500ms backoff, via the shared transport.
|
|
22
|
+
*/
|
|
23
|
+
import { postJsonWithRetry } from "./http.js";
|
|
24
|
+
const DEFAULT_BASE_URL = "https://api.openai.com/v1/chat/completions";
|
|
25
|
+
const RETRY_BACKOFF_MS = 500;
|
|
26
|
+
const RETRY_STATUSES = [429, 500, 502, 503, 504];
|
|
27
|
+
export class OpenAIProvider {
|
|
28
|
+
config;
|
|
29
|
+
name = "openai";
|
|
30
|
+
apiKey;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
if (!config.apiKey)
|
|
34
|
+
throw new Error("OpenAIProvider: apiKey is required");
|
|
35
|
+
if (!config.model)
|
|
36
|
+
throw new Error("OpenAIProvider: model is required");
|
|
37
|
+
this.apiKey = config.apiKey;
|
|
38
|
+
}
|
|
39
|
+
async select(req) {
|
|
40
|
+
const body = JSON.stringify({
|
|
41
|
+
model: this.config.model,
|
|
42
|
+
messages: [
|
|
43
|
+
{ role: "system", content: req.systemPrompt },
|
|
44
|
+
{ role: "user", content: req.userPrompt },
|
|
45
|
+
],
|
|
46
|
+
max_tokens: req.maxOutputTokens,
|
|
47
|
+
temperature: 0,
|
|
48
|
+
});
|
|
49
|
+
const headers = {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
52
|
+
};
|
|
53
|
+
const { json, httpStatus, latencyMs } = await postJsonWithRetry({
|
|
54
|
+
url: this.config.baseUrl ?? DEFAULT_BASE_URL,
|
|
55
|
+
headers,
|
|
56
|
+
body,
|
|
57
|
+
timeoutMs: req.timeoutMs,
|
|
58
|
+
retryStatuses: RETRY_STATUSES,
|
|
59
|
+
backoffMs: RETRY_BACKOFF_MS,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
rawText: extractText(json),
|
|
63
|
+
usage: extractUsage(json),
|
|
64
|
+
latencyMs,
|
|
65
|
+
httpStatus,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function extractText(json) {
|
|
70
|
+
if (!json)
|
|
71
|
+
return "";
|
|
72
|
+
const choices = json["choices"];
|
|
73
|
+
if (!Array.isArray(choices))
|
|
74
|
+
return "";
|
|
75
|
+
const first = choices[0];
|
|
76
|
+
const content = first?.message?.content;
|
|
77
|
+
return typeof content === "string" ? content : "";
|
|
78
|
+
}
|
|
79
|
+
function extractUsage(json) {
|
|
80
|
+
const usage = json?.["usage"] ?? {};
|
|
81
|
+
const num = (key) => {
|
|
82
|
+
const v = usage[key];
|
|
83
|
+
return typeof v === "number" ? v : 0;
|
|
84
|
+
};
|
|
85
|
+
const details = usage["prompt_tokens_details"] ?? {};
|
|
86
|
+
const cachedTokens = typeof details["cached_tokens"] === "number" ? details["cached_tokens"] : 0;
|
|
87
|
+
return {
|
|
88
|
+
inputTokens: num("prompt_tokens"),
|
|
89
|
+
outputTokens: num("completion_tokens"),
|
|
90
|
+
cacheCreateTokens: 0,
|
|
91
|
+
cacheReadTokens: cachedTokens,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/providers/openai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface — memhook abstracts the LLM call behind a common shape.
|
|
3
|
+
*
|
|
4
|
+
* The contract is deliberately provider-agnostic: a provider takes a
|
|
5
|
+
* `SelectionRequest` (system prompt + user prompt + caps) and returns a
|
|
6
|
+
* normalised `SelectionResponse` (raw text + token usage + latency). The
|
|
7
|
+
* router owns parsing the raw text into a basename array, so every provider
|
|
8
|
+
* looks identical from the pipeline's point of view.
|
|
9
|
+
*
|
|
10
|
+
* Provider-specific concepts must NOT leak here. Anthropic's ephemeral cache
|
|
11
|
+
* control and `anthropic-beta` headers live in `AnthropicProviderOptions`
|
|
12
|
+
* (passed to the Anthropic adapter's constructor), never on this interface.
|
|
13
|
+
*
|
|
14
|
+
* v0.2 ships AnthropicProvider, OpenAIProvider and OllamaProvider, built via
|
|
15
|
+
* `createProvider()` in `factory.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export interface ProviderConfig {
|
|
18
|
+
/** API key. Optional — local providers (Ollama) require none. */
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
model: string;
|
|
21
|
+
/** Override the provider's default API endpoint. */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SelectionRequest {
|
|
25
|
+
systemPrompt: string;
|
|
26
|
+
userPrompt: string;
|
|
27
|
+
maxOutputTokens: number;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
}
|
|
30
|
+
export interface UsageBreakdown {
|
|
31
|
+
inputTokens: number;
|
|
32
|
+
outputTokens: number;
|
|
33
|
+
/** Cache-write tokens. 0 for providers without explicit cache control. */
|
|
34
|
+
cacheCreateTokens: number;
|
|
35
|
+
/** Cache-read tokens (Anthropic `cache_read`, OpenAI `cached_tokens`). */
|
|
36
|
+
cacheReadTokens: number;
|
|
37
|
+
}
|
|
38
|
+
export interface SelectionResponse {
|
|
39
|
+
rawText: string;
|
|
40
|
+
usage: UsageBreakdown;
|
|
41
|
+
latencyMs: number;
|
|
42
|
+
httpStatus: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Provider {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
select(req: SelectionRequest): Promise<SelectionResponse>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC3D"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface — memhook abstracts the LLM call behind a common shape.
|
|
3
|
+
*
|
|
4
|
+
* The contract is deliberately provider-agnostic: a provider takes a
|
|
5
|
+
* `SelectionRequest` (system prompt + user prompt + caps) and returns a
|
|
6
|
+
* normalised `SelectionResponse` (raw text + token usage + latency). The
|
|
7
|
+
* router owns parsing the raw text into a basename array, so every provider
|
|
8
|
+
* looks identical from the pipeline's point of view.
|
|
9
|
+
*
|
|
10
|
+
* Provider-specific concepts must NOT leak here. Anthropic's ephemeral cache
|
|
11
|
+
* control and `anthropic-beta` headers live in `AnthropicProviderOptions`
|
|
12
|
+
* (passed to the Anthropic adapter's constructor), never on this interface.
|
|
13
|
+
*
|
|
14
|
+
* v0.2 ships AnthropicProvider, OpenAIProvider and OllamaProvider, built via
|
|
15
|
+
* `createProvider()` in `factory.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=types.js.map
|