@haoyiyin/9router 0.1.4 → 0.1.5
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 +18 -0
- package/package.json +1 -1
- package/src/config.ts +15 -1
- package/src/data.ts +4 -0
- package/src/provider.ts +64 -3
package/README.md
CHANGED
|
@@ -16,6 +16,24 @@ There is no built-in default endpoint. Run `/9router`, enter the 9Router base UR
|
|
|
16
16
|
|
|
17
17
|
Environment variables are supported as a fallback: `ROUTER_API_BASE` / `ROUTER_API_KEY` (the config file takes precedence).
|
|
18
18
|
|
|
19
|
+
## Combo models & thinking
|
|
20
|
+
|
|
21
|
+
Combo models (`sub` / `free` / `main`, `owned_by: "combo"`) advertise no capabilities, so reasoning is off by default. If every upstream in a combo route supports thinking, opt in via `comboReasoning` in `~/.pi/agent/haoyiyin/9router.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"comboReasoning": true
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`true` enables the default `off..high` range. To unlock `max`/`xhigh`, pass a custom `thinkingLevelMap` (keys: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; values: what gets sent to the router, `null` hides the level):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"comboReasoning": { "thinkingLevelMap": { "max": "max", "xhigh": "xhigh" } }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
19
37
|
## oc models
|
|
20
38
|
|
|
21
39
|
Every 30 minutes the extension fetches models from `opencode.ai/zen/v1/models` whose ids end in `-free`, and registers them under the 9router provider as `oc/<id>`. Changes to the upstream list are picked up automatically. Cache file: `~/.cache/pi/zen-oc-models.json`.
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -7,6 +7,13 @@ export const DEFAULT_BASE = "";
|
|
|
7
7
|
export interface RouterConfig {
|
|
8
8
|
baseUrl: string;
|
|
9
9
|
apiKey: string;
|
|
10
|
+
/**
|
|
11
|
+
* Opt-in to force reasoning:true onto combo models (`owned_by === "combo"`).
|
|
12
|
+
* Combo models carry no capabilities from the router, so reasoning is normally
|
|
13
|
+
* off. Set `true` for the default off..high range, or pass a custom
|
|
14
|
+
* `thinkingLevelMap` to unlock max/xhigh on the combo routes.
|
|
15
|
+
*/
|
|
16
|
+
comboReasoning?: boolean | { thinkingLevelMap?: Record<string, string | null> };
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export function configPath(): string {
|
|
@@ -17,6 +24,7 @@ export function loadConfig(): RouterConfig {
|
|
|
17
24
|
const path = configPath();
|
|
18
25
|
let fileBase: string | undefined;
|
|
19
26
|
let fileKey: string | undefined;
|
|
27
|
+
let comboReasoning: RouterConfig["comboReasoning"] | undefined;
|
|
20
28
|
|
|
21
29
|
if (existsSync(path)) {
|
|
22
30
|
try {
|
|
@@ -24,6 +32,12 @@ export function loadConfig(): RouterConfig {
|
|
|
24
32
|
if (parsed && typeof parsed === "object") {
|
|
25
33
|
if (typeof parsed.baseUrl === "string") fileBase = parsed.baseUrl;
|
|
26
34
|
if (typeof parsed.apiKey === "string") fileKey = parsed.apiKey;
|
|
35
|
+
if (
|
|
36
|
+
typeof parsed.comboReasoning === "boolean" ||
|
|
37
|
+
(parsed.comboReasoning && typeof parsed.comboReasoning === "object")
|
|
38
|
+
) {
|
|
39
|
+
comboReasoning = parsed.comboReasoning;
|
|
40
|
+
}
|
|
27
41
|
}
|
|
28
42
|
} catch {
|
|
29
43
|
// tolerate malformed config -> fall back to env/defaults
|
|
@@ -35,7 +49,7 @@ export function loadConfig(): RouterConfig {
|
|
|
35
49
|
|
|
36
50
|
const apiKey = fileKey ?? process.env.ROUTER_API_KEY ?? "";
|
|
37
51
|
|
|
38
|
-
return { baseUrl, apiKey };
|
|
52
|
+
return { baseUrl, apiKey, comboReasoning };
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export function saveConfig(cfg: RouterConfig): void {
|
package/src/data.ts
CHANGED
package/src/provider.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-codin
|
|
|
13
13
|
import type { ModelsDevModel, RouterModel } from "./data.ts";
|
|
14
14
|
import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.ts";
|
|
15
15
|
import { getZenFreeModels } from "./zen.ts";
|
|
16
|
-
import { loadConfig } from "./config.ts";
|
|
16
|
+
import { loadConfig, type RouterConfig } from "./config.ts";
|
|
17
17
|
|
|
18
18
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
19
19
|
const DEFAULT_MAX_TOKENS = 16_384;
|
|
@@ -65,10 +65,61 @@ export function getMaxTokens(model: RouterModel, devModel?: ModelsDevModel): num
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boolean {
|
|
68
|
+
if (typeof model.capabilities?.reasoning === "boolean") return model.capabilities.reasoning;
|
|
68
69
|
if (typeof devModel?.reasoning === "boolean") return devModel.reasoning;
|
|
69
70
|
return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet/i.test(model.id ?? "");
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// pi understands these thinkingFormats; the router backend declares openai/deepseek
|
|
74
|
+
// (and occasionally minimax/gemini-level, which pi cannot natively drive). Map the
|
|
75
|
+
// known ones and fall back to openai for anything unknown/minimax/gemini-level.
|
|
76
|
+
const PI_THINKING_FORMAT: Record<string, "openai" | "deepseek"> = {
|
|
77
|
+
openai: "openai",
|
|
78
|
+
deepseek: "deepseek",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function getThinkingFormat(model: RouterModel): "openai" | "deepseek" {
|
|
82
|
+
return PI_THINKING_FORMAT[model.capabilities?.thinkingFormat ?? ""] ?? "openai";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derive pi's thinkingLevelMap from the router's advertised capabilities.
|
|
87
|
+
* DeepSeek exposes low/high/max (mirrors pi's own deepseek catalog); openai-style
|
|
88
|
+
* models keep the default off..high range, only hiding `off` when thinking cannot
|
|
89
|
+
* be disabled. No map is returned when the backend does not declare reasoning.
|
|
90
|
+
*/
|
|
91
|
+
export function getThinkingLevelMap(
|
|
92
|
+
model: RouterModel,
|
|
93
|
+
): ProviderModelConfig["thinkingLevelMap"] | undefined {
|
|
94
|
+
const cap = model.capabilities;
|
|
95
|
+
if (cap?.reasoning !== true) return undefined;
|
|
96
|
+
if (cap.thinkingFormat === "deepseek") {
|
|
97
|
+
return { minimal: null, low: "low", medium: null, high: "high", max: "max" };
|
|
98
|
+
}
|
|
99
|
+
if (cap.thinkingCanDisable === false) return { off: null };
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Combo models (`owned_by === "combo"`) carry no capabilities from the router, so
|
|
105
|
+
* reasoning is normally off. Operators who guarantee their combo routes only hit
|
|
106
|
+
* thinking-capable upstreams can opt in via `comboReasoning` in 9router.json:
|
|
107
|
+
* `true` for the default off..high range, or an object with a custom
|
|
108
|
+
* `thinkingLevelMap` to unlock max/xhigh.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveComboOptIn(
|
|
111
|
+
comboReasoning: RouterConfig["comboReasoning"],
|
|
112
|
+
): { reasoning: true; thinkingLevelMap: ProviderModelConfig["thinkingLevelMap"] | undefined } | null {
|
|
113
|
+
if (!comboReasoning) return null;
|
|
114
|
+
return {
|
|
115
|
+
reasoning: true,
|
|
116
|
+
thinkingLevelMap:
|
|
117
|
+
comboReasoning === true
|
|
118
|
+
? undefined
|
|
119
|
+
: (comboReasoning.thinkingLevelMap as ProviderModelConfig["thinkingLevelMap"]),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
72
123
|
/**
|
|
73
124
|
* True if `err` is pi's stale-extension-ctx error, thrown when a captured
|
|
74
125
|
* `pi`/command ctx is used after session replacement (newSession / fork /
|
|
@@ -118,15 +169,25 @@ export async function registerProvider(pi: ExtensionAPI): Promise<void> {
|
|
|
118
169
|
if (!id || seen.has(id)) continue;
|
|
119
170
|
seen.add(id);
|
|
120
171
|
const devModel = lookupInIndex(id, devIndex);
|
|
172
|
+
const thinkingFormat = getThinkingFormat(model);
|
|
173
|
+
const comboOptIn = model.owned_by === "combo" ? resolveComboOptIn(cfg.comboReasoning) : null;
|
|
121
174
|
out.push({
|
|
122
175
|
id,
|
|
123
176
|
name: getModelName(model, devModel),
|
|
124
|
-
reasoning: getReasoning(model, devModel),
|
|
177
|
+
reasoning: comboOptIn ? comboOptIn.reasoning : getReasoning(model, devModel),
|
|
125
178
|
input: getInputTypes(model, devModel),
|
|
126
179
|
cost: ZERO_COST,
|
|
127
180
|
contextWindow: getContextWindow(model, devModel),
|
|
128
181
|
maxTokens: getMaxTokens(model, devModel),
|
|
129
|
-
|
|
182
|
+
thinkingLevelMap: comboOptIn ? comboOptIn.thinkingLevelMap : getThinkingLevelMap(model),
|
|
183
|
+
compat: {
|
|
184
|
+
...COMPAT,
|
|
185
|
+
supportsReasoningEffort: true,
|
|
186
|
+
thinkingFormat,
|
|
187
|
+
...(thinkingFormat === "deepseek"
|
|
188
|
+
? { requiresReasoningContentOnAssistantMessages: true }
|
|
189
|
+
: {}),
|
|
190
|
+
},
|
|
130
191
|
});
|
|
131
192
|
}
|
|
132
193
|
|