@haoyiyin/9router 0.1.6 → 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/README.md CHANGED
@@ -12,7 +12,7 @@ pi install npm:@haoyiyin/9router
12
12
 
13
13
  ## Configure
14
14
 
15
- There is no built-in default endpoint. Run `/9router`, enter the 9Router base URL and API key, and they are saved to `~/.pi/agent/haoyiyin/9router.json` (mode `0600`). Leave the key empty to keep the currently configured key.
15
+ There is no built-in default endpoint. Run `/login`, select **9Router**, and enter the base URL and API key. They are saved to `~/.pi/agent/haoyiyin/9router.json` (mode `0600`). `/logout` removes the stored credential.
16
16
 
17
17
  Environment variables are supported as a fallback: `ROUTER_API_BASE` / `ROUTER_API_KEY` (the config file takes precedence).
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haoyiyin/9router",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Pi extension — 9Router provider (OpenCode Free oc/-free models + router models) + fetch/search/transcribe tools",
package/src/index.ts CHANGED
@@ -3,15 +3,12 @@ import registerFetch from "./fetch.ts";
3
3
  import registerSearch from "./search.ts";
4
4
  import registerTranscribe from "./transcribe.ts";
5
5
  import { registerProvider, isStaleCtxError } from "./provider.ts";
6
- import { registerCommand } from "./command.ts";
7
6
 
8
7
  export default async function (pi: ExtensionAPI): Promise<void> {
9
8
  registerFetch(pi);
10
9
  registerSearch(pi);
11
10
  registerTranscribe(pi);
12
11
 
13
- registerCommand(pi);
14
-
15
12
  await registerProvider(pi);
16
13
 
17
14
  let refreshing = false;
package/src/provider.ts CHANGED
@@ -3,20 +3,26 @@
3
3
  *
4
4
  * Registers the "9router" provider in Pi, pulling the live router model list
5
5
  * plus OpenCode's free oc/-free models. Falls back to an empty model list if
6
- * the API key is unset (so the /9router command can re-register after config
6
+ * the API key is unset (so /login can configure it and re-register once a key
7
7
  * is saved).
8
8
  *
9
9
  * Config (base URL + key, with env fallback) is owned by ./config.ts.
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
13
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
13
14
  import type { ModelsDevModel, RouterModel } from "./data.ts";
14
15
  import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.ts";
15
16
  import { getZenFreeModels } from "./zen.ts";
16
- import { loadConfig, type RouterConfig } from "./config.ts";
17
+ import { loadConfig, saveConfig, type RouterConfig } from "./config.ts";
17
18
 
18
- const DEFAULT_CONTEXT_WINDOW = 128_000;
19
- const DEFAULT_MAX_TOKENS = 16_384;
19
+ // ponytail: 128k was the old generic fallback; combo members bottom out at 200k,
20
+ // so the default now matches the smallest combo upstream. Raise again if the
21
+ // router ever exposes combo capabilities.contextWindow.
22
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
23
+ // ponytail: 16k was the old generic fallback; combo members bottom out at 128k,
24
+ // so the default now matches the smallest combo upstream max output.
25
+ const DEFAULT_MAX_TOKENS = 128_000;
20
26
 
21
27
  const ZERO_COST = {
22
28
  input: 0,
@@ -134,17 +140,56 @@ export function isStaleCtxError(err: unknown): boolean {
134
140
  return err instanceof Error && /stale after session replacement|ctx is stale/.test(err.message);
135
141
  }
136
142
 
143
+ // Login flow: collect base URL + API key, persist to config, then re-register
144
+ // so the model list and base URL take effect. The key is carried as the OAuth
145
+ // credential (getApiKey); no separate apiKey is set on the provider, so /login
146
+ // shows exactly one 9router entry instead of an api_key + oauth pair.
147
+ function makeOAuth(pi: ExtensionAPI) {
148
+ return {
149
+ name: "9Router",
150
+ login: async (callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> => {
151
+ const current = loadConfig();
152
+ const baseUrl = await callbacks.onPrompt({
153
+ message: "9Router base URL",
154
+ placeholder: current.baseUrl || "https://your-router.example.com",
155
+ });
156
+ if (baseUrl === undefined) throw new Error("9Router login cancelled");
157
+ const trimmedBase = baseUrl.trim();
158
+ const normalizedBase = trimmedBase.endsWith("/")
159
+ ? trimmedBase.slice(0, -1)
160
+ : trimmedBase;
161
+
162
+ const apiKey = await callbacks.onPrompt({
163
+ message: "9Router API key",
164
+ placeholder: "sk-...",
165
+ });
166
+ if (apiKey === undefined) throw new Error("9Router login cancelled");
167
+ const trimmedKey = apiKey.trim();
168
+ if (!trimmedKey) throw new Error("9Router API key is required");
169
+
170
+ saveConfig({ baseUrl: normalizedBase, apiKey: trimmedKey, comboReasoning: current.comboReasoning });
171
+
172
+ // Re-register with the freshly saved config so models + base URL apply.
173
+ await registerProvider(pi);
174
+
175
+ return { refresh: trimmedKey, access: trimmedKey, expires: Number.MAX_SAFE_INTEGER };
176
+ },
177
+ refreshToken: async (cred: OAuthCredentials): Promise<OAuthCredentials> => cred,
178
+ getApiKey: (cred: OAuthCredentials): string => cred.access,
179
+ };
180
+ }
181
+
137
182
  export async function registerProvider(pi: ExtensionAPI): Promise<void> {
138
183
  const cfg = loadConfig();
184
+ const oauth = makeOAuth(pi);
139
185
 
140
186
  if (!cfg.apiKey) {
141
- // Register shell provider so the name is known; no models available yet.
187
+ // Register shell provider so /login can configure it; no models yet.
142
188
  pi.registerProvider("9router", {
143
189
  name: "9Router",
144
- baseUrl: cfg.baseUrl,
145
- apiKey: "",
146
190
  api: "openai-completions",
147
191
  models: [],
192
+ oauth,
148
193
  });
149
194
  return;
150
195
  }
@@ -223,10 +268,10 @@ export async function registerProvider(pi: ExtensionAPI): Promise<void> {
223
268
  pi.registerProvider("9router", {
224
269
  name: "9Router",
225
270
  baseUrl: cfg.baseUrl,
226
- apiKey: cfg.apiKey,
227
271
  api: "openai-completions",
228
272
  headers: { "User-Agent": "pi-coding-agent" },
229
273
  models: out,
274
+ oauth,
230
275
  });
231
276
  }
232
277
 
package/src/command.ts DELETED
@@ -1,54 +0,0 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { loadConfig, saveConfig, DEFAULT_BASE } from "./config.ts";
3
- import { registerProvider, isStaleCtxError } from "./provider.ts";
4
-
5
- // Never contains a real key. If a dialog pre-fills this placeholder as an
6
- // editable default, an exact match is treated as "keep the current key".
7
- const API_KEY_PLACEHOLDER = "sk-... (leave empty to keep the current key)";
8
-
9
- export function registerCommand(pi: ExtensionAPI): void {
10
- pi.registerCommand("9router", {
11
- description: "Configure 9Router base URL and API key (saved to ~/.pi/agent/haoyiyin/9router.json)",
12
- handler: async (args: string, ctx: ExtensionCommandContext) => {
13
- const current = loadConfig();
14
-
15
- const baseUrl = await ctx.ui.input("9Router base URL", current.baseUrl || DEFAULT_BASE);
16
- if (baseUrl === undefined) return;
17
- const trimmedBase = baseUrl.trim();
18
- const normalizedBase = trimmedBase.endsWith("/")
19
- ? trimmedBase.slice(0, -1)
20
- : trimmedBase;
21
-
22
- const apiKey = await ctx.ui.input("9Router API key", API_KEY_PLACEHOLDER);
23
- if (apiKey === undefined) return;
24
- const trimmedKey = apiKey.trim();
25
-
26
- let finalKey: string;
27
- if (trimmedKey === "" || trimmedKey === API_KEY_PLACEHOLDER) {
28
- // Keep the existing key; never treat the placeholder as a submitted value.
29
- if (!current.apiKey) {
30
- ctx.ui.notify("No API key configured", "error");
31
- return;
32
- }
33
- finalKey = current.apiKey;
34
- } else {
35
- finalKey = trimmedKey;
36
- }
37
-
38
- saveConfig({ baseUrl: normalizedBase, apiKey: finalKey });
39
-
40
- try {
41
- await registerProvider(pi);
42
- } catch (err) {
43
- ctx.ui.notify(
44
- isStaleCtxError(err)
45
- ? "9Router saved. This extension instance was replaced by a session change/reload; the new instance has re-registered the provider."
46
- : `9Router saved, but provider re-registration failed: ${err instanceof Error ? err.message : String(err)}`,
47
- "warning",
48
- );
49
- }
50
-
51
- ctx.ui.notify(`9Router saved: ${normalizedBase}`, "info");
52
- },
53
- });
54
- }