@plurnk/plurnk-aliases 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.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @plurnk/plurnk-aliases
2
+
3
+ Zero-dependency parser for the plurnk model-alias cascade. Vendor-agnostic, MIT.
4
+
5
+ Resolves `PLURNK_MODEL_<alias>=<provider>/<model>` env vars (plus per-alias
6
+ `PLURNK_BASEURL_<alias>` endpoint overrides) into structured `ProviderAlias`
7
+ records. Extracted from `@plurnk/plurnk-providers` so a **thin client** can
8
+ resolve aliases from its own always-fresh env — and send the daemon a resolved
9
+ `{ provider, model, baseUrl? }` — without pulling the provider-instantiation or
10
+ tokenizer machinery. `@plurnk/plurnk-providers` depends on this and re-exports
11
+ the same surface, so it stays the single source of truth.
12
+
13
+ ## Install
14
+
15
+ ```
16
+ npm install @plurnk/plurnk-aliases
17
+ ```
18
+
19
+ Node ≥26, ESM.
20
+
21
+ ## API
22
+
23
+ ```ts
24
+ import { parseAliasesFromEnv, resolveActiveAlias, type ProviderAlias } from "@plurnk/plurnk-aliases";
25
+
26
+ parseAliasesFromEnv(env = process.env): ProviderAlias[] // every declared alias
27
+ resolveActiveAlias(env = process.env): ProviderAlias | null // the PLURNK_MODEL-selected one, or null
28
+ ```
29
+
30
+ ```ts
31
+ interface ProviderAlias {
32
+ readonly alias: string; // lowercase, .env key suffix downcased
33
+ readonly provider: string; // "openai", "openrouter", "ollama", …
34
+ readonly model: string; // provider-native id; may contain "/"
35
+ readonly baseUrl?: string; // PLURNK_BASEURL_<alias> override, when set
36
+ }
37
+ ```
38
+
39
+ To resolve a named alias (e.g. from `loop.run({ alias })`), parse and find:
40
+
41
+ ```ts
42
+ const alias = parseAliasesFromEnv().find((a) => a.alias === name.toLowerCase());
43
+ ```
44
+
45
+ ## Contract
46
+
47
+ The provider segment is the **first** `/`-delimited field; the model id is the
48
+ remainder (it may itself contain `/`). Aliases are case-folded. Fail-hard on a
49
+ case-folding collision (two keys → one alias) and on a `PLURNK_BASEURL_*`
50
+ override with no matching alias (a typo, never a silent no-op). See `SPEC.md`.
package/SPEC.md ADDED
@@ -0,0 +1,47 @@
1
+ # @plurnk/plurnk-aliases — contract
2
+
3
+ The canonical parser for the plurnk model-alias cascade. Zero runtime
4
+ dependencies. `@plurnk/plurnk-providers` depends on this package and re-exports
5
+ its surface unchanged; thin clients depend on it directly to resolve aliases
6
+ without the provider/tokenizer machinery.
7
+
8
+ ## The cascade
9
+
10
+ - **`PLURNK_MODEL_<alias>=<provider>/<model>`** declares an alias. The provider
11
+ segment is the **first** `/`-delimited field; the model id is everything after
12
+ that first `/` (it MAY contain further `/`, e.g. `openrouter/anthropic/claude-…`).
13
+ - **`PLURNK_MODEL=<alias>`** selects the active alias at boot.
14
+ - **`PLURNK_BASEURL_<alias>`** attaches a per-alias endpoint override — the one
15
+ thing a per-provider base-URL var can't express (two aliases on the same
16
+ provider name pointing at different self-hosted boxes).
17
+
18
+ Alias keys are **case-folded** (the suffix after `PLURNK_MODEL_` / `PLURNK_BASEURL_`
19
+ is downcased), so `PLURNK_MODEL_opus` and `PLURNK_MODEL_OPUS` name the same alias.
20
+
21
+ ## `ProviderAlias`
22
+
23
+ ```ts
24
+ interface ProviderAlias {
25
+ readonly alias: string;
26
+ readonly provider: string;
27
+ readonly model: string;
28
+ readonly baseUrl?: string; // present only when PLURNK_BASEURL_<alias> is set
29
+ }
30
+ ```
31
+
32
+ ## Functions
33
+
34
+ - **`parseAliasesFromEnv(env = process.env): ProviderAlias[]`** — every declared
35
+ alias. Skips entries with an empty value or no `/` in the value.
36
+ - **`resolveActiveAlias(env = process.env): ProviderAlias | null`** — the alias
37
+ named by `PLURNK_MODEL` (case-insensitive), or `null` when `PLURNK_MODEL` is
38
+ unset or names no declared alias.
39
+
40
+ ## Fail-hard rules
41
+
42
+ - **Case-folding collision** — two `PLURNK_MODEL_*` keys that downcase to the
43
+ same alias throw (`Duplicate provider alias "<alias>"`). No silent pick.
44
+ - **Dangling override** — a `PLURNK_BASEURL_*` whose alias has no matching
45
+ `PLURNK_MODEL_*` throws (a typo, not a silent no-op).
46
+
47
+ Both are contract violations surfaced loudly, never recovered.
@@ -0,0 +1,4 @@
1
+ import type { ProviderAlias } from "./types.ts";
2
+ export declare const parseAliasesFromEnv: (env?: NodeJS.ProcessEnv) => ProviderAlias[];
3
+ export declare const resolveActiveAlias: (env?: NodeJS.ProcessEnv) => ProviderAlias | null;
4
+ //# sourceMappingURL=aliases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aliases.d.ts","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAkBhD,eAAO,MAAM,mBAAmB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,aAAa,EAuBvF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,aAAa,GAAG,IAKzF,CAAC"}
@@ -0,0 +1,67 @@
1
+ // The plurnk model-alias cascade — pure env parsing, zero runtime deps.
2
+ //
3
+ // PLURNK_MODEL_<alias>=<provider>/<model> declares an alias; PLURNK_MODEL=<alias>
4
+ // selects the active one at boot. The provider segment is the first "/"-delimited
5
+ // field; the model id is the remainder (it may itself contain "/"). Aliases are
6
+ // case-folded (the .env key suffix downcased). PLURNK_BASEURL_<alias> attaches a
7
+ // per-alias endpoint override — the one thing a per-provider base-URL var can't
8
+ // express (two aliases on the same provider name pointing at different boxes).
9
+ //
10
+ // Extracted from @plurnk/plurnk-providers so a thin consumer can resolve aliases
11
+ // from its own (always-fresh) env without pulling the provider/tokenizer machinery.
12
+ // PLURNK_BASEURL_<alias>: per-alias endpoint override, case-folded on the alias
13
+ // to match PLURNK_MODEL_<alias>. Lets two aliases on the same provider name target
14
+ // different self-hosted boxes (openai/ollama), the one thing a per-name base-URL
15
+ // var can't express.
16
+ const parseBaseUrls = (env) => {
17
+ const out = new Map();
18
+ for (const [key, value] of Object.entries(env)) {
19
+ if (value === undefined || value.length === 0)
20
+ continue;
21
+ if (!key.startsWith("PLURNK_BASEURL_"))
22
+ continue;
23
+ const aliasRaw = key.slice("PLURNK_BASEURL_".length);
24
+ if (aliasRaw.length === 0)
25
+ continue;
26
+ out.set(aliasRaw.toLowerCase(), value);
27
+ }
28
+ return out;
29
+ };
30
+ export const parseAliasesFromEnv = (env = process.env) => {
31
+ const out = [];
32
+ const seen = new Set();
33
+ const baseUrls = parseBaseUrls(env);
34
+ for (const [key, value] of Object.entries(env)) {
35
+ if (value === undefined || value.length === 0)
36
+ continue;
37
+ if (!key.startsWith("PLURNK_MODEL_"))
38
+ continue;
39
+ const aliasRaw = key.slice("PLURNK_MODEL_".length);
40
+ if (aliasRaw.length === 0)
41
+ continue;
42
+ const slash = value.indexOf("/");
43
+ if (slash <= 0)
44
+ continue;
45
+ const alias = aliasRaw.toLowerCase();
46
+ // Aliases are case-folded, so PLURNK_MODEL_opus and PLURNK_MODEL_OPUS
47
+ // collide. Surface the ambiguity rather than silently picking one.
48
+ if (seen.has(alias))
49
+ throw new Error(`Duplicate provider alias "${alias}": multiple PLURNK_MODEL_* keys case-fold to the same alias. Rename one.`);
50
+ seen.add(alias);
51
+ const baseUrl = baseUrls.get(alias);
52
+ out.push({ alias, provider: value.slice(0, slash), model: value.slice(slash + 1), ...(baseUrl !== undefined ? { baseUrl } : {}) });
53
+ }
54
+ // A base-URL override with no matching alias is a typo, not a silent no-op.
55
+ const unmatched = [...baseUrls.keys()].filter((a) => !seen.has(a));
56
+ if (unmatched.length > 0)
57
+ throw new Error(`PLURNK_BASEURL_* override(s) with no matching PLURNK_MODEL_* alias: ${unmatched.join(", ")}. Declare the alias or remove the override.`);
58
+ return out;
59
+ };
60
+ export const resolveActiveAlias = (env = process.env) => {
61
+ const selected = env.PLURNK_MODEL;
62
+ if (selected === undefined || selected.length === 0)
63
+ return null;
64
+ const aliases = parseAliasesFromEnv(env);
65
+ return aliases.find((a) => a.alias === selected.toLowerCase()) ?? null;
66
+ };
67
+ //# sourceMappingURL=aliases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aliases.js","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type { ProviderAlias } from "./types.ts";
2
+ export { parseAliasesFromEnv, resolveActiveAlias } from "./aliases.ts";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { parseAliasesFromEnv, resolveActiveAlias } from "./aliases.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface ProviderAlias {
2
+ readonly alias: string;
3
+ readonly provider: string;
4
+ readonly model: string;
5
+ readonly baseUrl?: string;
6
+ }
7
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC7B"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@plurnk/plurnk-aliases",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency parser for the plurnk model-alias cascade (PLURNK_MODEL_<alias>=<provider>/<model>, PLURNK_BASEURL_<alias>). Shared by @plurnk/plurnk-providers and thin plurnk clients that resolve aliases without pulling the provider/tokenizer machinery.",
5
+ "keywords": [
6
+ "plurnk",
7
+ "alias",
8
+ "provider",
9
+ "env"
10
+ ],
11
+ "homepage": "https://github.com/plurnk/plurnk-aliases#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/plurnk/plurnk-aliases/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/plurnk/plurnk-aliases.git"
18
+ },
19
+ "engines": {
20
+ "node": ">=26"
21
+ },
22
+ "license": "MIT",
23
+ "author": "@wikitopian",
24
+ "type": "module",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist/**/*",
37
+ "README.md",
38
+ "SPEC.md"
39
+ ],
40
+ "scripts": {
41
+ "test:lint": "tsc --noEmit",
42
+ "test:unit": "node --test src/**/*.test.ts",
43
+ "test": "npm run test:lint && npm run test:unit",
44
+ "build:dist": "tsc -p tsconfig.build.json",
45
+ "build": "npm run build:dist",
46
+ "prepare": "npm run build"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.0.0",
50
+ "typescript": "^6.0.3"
51
+ }
52
+ }