pi-freeflow 1.28.0 → 1.28.1

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 (3) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -1
  3. package/src/probe.ts +153 -153
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.28.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Retry relay reachability checks after deployment so edge routing propagation delays do not show a fresh relay as unreachable — the check retries with progress notices and only warns if the relay stays dark.
8
+
9
+ Fix upstream drift alerts to stop flagging commercial paid model families as free candidates, while known alpha and stealth free models keep their free status.
10
+
3
11
  ## 1.28.0
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.28.0",
4
+ "version": "1.28.1",
5
5
  "description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
6
6
  "main": "extensions/index.ts",
7
7
  "types": "src/index.ts",
package/src/probe.ts CHANGED
@@ -1,153 +1,153 @@
1
- import { opencodeHeaders } from "./config.ts";
2
- import { ensureChatFingerprintTools } from "./opencode-fingerprint.ts";
3
-
4
- /**
5
- * Relay reachability probe: verifies a deployed relay answers requests
6
- * by issuing a minimal round trip through it and timing the response.
7
- */
8
-
9
- export interface RelayProbeResult {
10
- ok: boolean;
11
- status: number;
12
- latencyMs: number;
13
- error?: string;
14
- }
15
-
16
- export interface RelayProbeOptions {
17
- /**
18
- * Probe mode:
19
- * - "models" (default): GET /v1/models (reachability probe)
20
- * - "chat": POST /zen/v1/chat/completions (verifies free tier inference & spoofing)
21
- */
22
- endpoint?: "models" | "chat";
23
- /** Model to use for chat probe (defaults to "nemotron-3.5-lightning-free") */
24
- model?: string;
25
- /** Timeout in milliseconds (defaults to 5000) */
26
- timeoutMs?: number;
27
- /**
28
- * Number of retry attempts when the probe returns !ok or throws.
29
- * Defaults to 0 (single attempt). Useful post-deploy for edge DNS propagation.
30
- */
31
- retries?: number;
32
- /** Delay between retries in milliseconds (defaults to 2000). */
33
- retryDelayMs?: number;
34
- /** Optional progress callback before each retry */
35
- onRetry?: (attempt: number, maxRetries: number, lastResult: RelayProbeResult) => void;
36
- }
37
-
38
- /**
39
- * Probe a relay endpoint to confirm it is reachable.
40
- *
41
- * Sends a minimal request to `<url>/v1/models` carrying the x-relay-target /
42
- * x-relay-path headers the relay expects, timing the round trip. Never
43
- * throws: network failures, timeouts, and non-2xx statuses are all reported
44
- * on the returned result instead.
45
- *
46
- * The path mirrors the proxy's real relay contract (`relayFetch` in relay.ts):
47
- * `x-relay-target` is the upstream origin and `x-relay-path` the full path
48
- * including the `/zen` prefix — the workers only allow the exact origins
49
- * `https://opencode.ai` / `https://api.kilo.ai` and forward `target + path`.
50
- * @param auth Optional per-relay shared secret; sent as x-relay-auth so
51
- * deployed relays (which enforce it) answer the probe instead of 401.
52
- */
53
- async function probeRelayOnce(
54
- url: string,
55
- auth?: string,
56
- opts?: RelayProbeOptions,
57
- ): Promise<RelayProbeResult> {
58
- const cleanUrl = url.trim().replace(/\/+$/, "");
59
- const isDirectOpenCode =
60
- cleanUrl === "https://opencode.ai" ||
61
- cleanUrl === "opencode" ||
62
- cleanUrl === "direct";
63
- const isChat = opts?.endpoint === "chat";
64
- const timeout = opts?.timeoutMs ?? (isChat ? 15_000 : 5_000);
65
- const start = Date.now();
66
-
67
- try {
68
- const headers: Record<string, string> = {
69
- ...opencodeHeaders(),
70
- ...(auth ? { "x-relay-auth": auth } : {}),
71
- };
72
-
73
- let fetchUrl: string;
74
- let method = "GET";
75
- let body: string | undefined;
76
-
77
- if (isDirectOpenCode) {
78
- if (isChat) {
79
- fetchUrl = "https://opencode.ai/zen/v1/chat/completions";
80
- method = "POST";
81
- headers["content-type"] = "application/json";
82
- const probePayload: Record<string, unknown> = {
83
- model: opts?.model || "nemotron-3.5-lightning-free",
84
- max_tokens: 1,
85
- stream: true,
86
- messages: [{ role: "user", content: "ping" }],
87
- };
88
- ensureChatFingerprintTools(probePayload);
89
- body = JSON.stringify(probePayload);
90
- } else {
91
- fetchUrl = "https://opencode.ai/zen/v1/models";
92
- }
93
- } else {
94
- if (isChat) {
95
- fetchUrl = `${cleanUrl}/zen/v1/chat/completions`;
96
- method = "POST";
97
- headers["content-type"] = "application/json";
98
- headers["x-relay-target"] = "https://opencode.ai";
99
- headers["x-relay-path"] = "/zen/v1/chat/completions";
100
- const probePayload: Record<string, unknown> = {
101
- model: opts?.model || "nemotron-3.5-lightning-free",
102
- max_tokens: 1,
103
- stream: true,
104
- messages: [{ role: "user", content: "ping" }],
105
- };
106
- ensureChatFingerprintTools(probePayload);
107
- body = JSON.stringify(probePayload);
108
- } else {
109
- fetchUrl = `${cleanUrl}/v1/models`;
110
- headers["x-relay-target"] = "https://opencode.ai";
111
- headers["x-relay-path"] = "/zen/v1/models";
112
- }
113
- }
114
-
115
- const res = await fetch(fetchUrl, {
116
- method,
117
- headers,
118
- body,
119
- signal: AbortSignal.timeout(timeout),
120
- });
121
- return { ok: res.ok, status: res.status, latencyMs: Date.now() - start };
122
- } catch (e) {
123
- return {
124
- ok: false,
125
- status: 0,
126
- latencyMs: Date.now() - start,
127
- error: (e as Error)?.message || String(e),
128
- };
129
- }
130
- }
131
-
132
- export async function probeRelay(
133
- url: string,
134
- auth?: string,
135
- opts?: RelayProbeOptions,
136
- ): Promise<RelayProbeResult> {
137
- const retries = Math.max(0, opts?.retries ?? 0);
138
- const retryDelay = Math.max(10, opts?.retryDelayMs ?? 2000);
139
-
140
- for (let attempt = 0; attempt <= retries; attempt++) {
141
- if (attempt > 0) {
142
- const { promise, resolve } = Promise.withResolvers<void>();
143
- setTimeout(resolve, retryDelay);
144
- await promise;
145
- }
146
- const result = await probeRelayOnce(url, auth, opts);
147
- if (result.ok || attempt === retries) {
148
- return result;
149
- }
150
- opts?.onRetry?.(attempt + 1, retries, result);
151
- }
152
- return { ok: false, status: 0, latencyMs: 0, error: "Probe failed" };
153
- }
1
+ import { opencodeHeaders } from "./config.ts";
2
+ import { ensureChatFingerprintTools } from "./opencode-fingerprint.ts";
3
+
4
+ /**
5
+ * Relay reachability probe: verifies a deployed relay answers requests
6
+ * by issuing a minimal round trip through it and timing the response.
7
+ */
8
+
9
+ export interface RelayProbeResult {
10
+ ok: boolean;
11
+ status: number;
12
+ latencyMs: number;
13
+ error?: string;
14
+ }
15
+
16
+ export interface RelayProbeOptions {
17
+ /**
18
+ * Probe mode:
19
+ * - "models" (default): GET /v1/models (reachability probe)
20
+ * - "chat": POST /zen/v1/chat/completions (verifies free tier inference & spoofing)
21
+ */
22
+ endpoint?: "models" | "chat";
23
+ /** Model to use for chat probe (defaults to "nemotron-3.5-lightning-free") */
24
+ model?: string;
25
+ /** Timeout in milliseconds (defaults to 5000) */
26
+ timeoutMs?: number;
27
+ /**
28
+ * Number of retry attempts when the probe returns !ok or throws.
29
+ * Defaults to 0 (single attempt). Useful post-deploy for edge DNS propagation.
30
+ */
31
+ retries?: number;
32
+ /** Delay between retries in milliseconds (defaults to 2000). */
33
+ retryDelayMs?: number;
34
+ /** Optional progress callback before each retry */
35
+ onRetry?: (attempt: number, maxRetries: number, lastResult: RelayProbeResult) => void;
36
+ }
37
+
38
+ /**
39
+ * Probe a relay endpoint to confirm it is reachable.
40
+ *
41
+ * Sends a minimal request to `<url>/v1/models` carrying the x-relay-target /
42
+ * x-relay-path headers the relay expects, timing the round trip. Never
43
+ * throws: network failures, timeouts, and non-2xx statuses are all reported
44
+ * on the returned result instead.
45
+ *
46
+ * The path mirrors the proxy's real relay contract (`relayFetch` in relay.ts):
47
+ * `x-relay-target` is the upstream origin and `x-relay-path` the full path
48
+ * including the `/zen` prefix — the workers only allow the exact origins
49
+ * `https://opencode.ai` / `https://api.kilo.ai` and forward `target + path`.
50
+ * @param auth Optional per-relay shared secret; sent as x-relay-auth so
51
+ * deployed relays (which enforce it) answer the probe instead of 401.
52
+ */
53
+ async function probeRelayOnce(
54
+ url: string,
55
+ auth?: string,
56
+ opts?: RelayProbeOptions,
57
+ ): Promise<RelayProbeResult> {
58
+ const cleanUrl = url.trim().replace(/\/+$/, "");
59
+ const isDirectOpenCode =
60
+ cleanUrl === "https://opencode.ai" ||
61
+ cleanUrl === "opencode" ||
62
+ cleanUrl === "direct";
63
+ const isChat = opts?.endpoint === "chat";
64
+ const timeout = opts?.timeoutMs ?? (isChat ? 15_000 : 5_000);
65
+ const start = Date.now();
66
+
67
+ try {
68
+ const headers: Record<string, string> = {
69
+ ...opencodeHeaders(),
70
+ ...(auth ? { "x-relay-auth": auth } : {}),
71
+ };
72
+
73
+ let fetchUrl: string;
74
+ let method = "GET";
75
+ let body: string | undefined;
76
+
77
+ if (isDirectOpenCode) {
78
+ if (isChat) {
79
+ fetchUrl = "https://opencode.ai/zen/v1/chat/completions";
80
+ method = "POST";
81
+ headers["content-type"] = "application/json";
82
+ const probePayload: Record<string, unknown> = {
83
+ model: opts?.model || "nemotron-3.5-lightning-free",
84
+ max_tokens: 1,
85
+ stream: true,
86
+ messages: [{ role: "user", content: "ping" }],
87
+ };
88
+ ensureChatFingerprintTools(probePayload);
89
+ body = JSON.stringify(probePayload);
90
+ } else {
91
+ fetchUrl = "https://opencode.ai/zen/v1/models";
92
+ }
93
+ } else {
94
+ if (isChat) {
95
+ fetchUrl = `${cleanUrl}/zen/v1/chat/completions`;
96
+ method = "POST";
97
+ headers["content-type"] = "application/json";
98
+ headers["x-relay-target"] = "https://opencode.ai";
99
+ headers["x-relay-path"] = "/zen/v1/chat/completions";
100
+ const probePayload: Record<string, unknown> = {
101
+ model: opts?.model || "nemotron-3.5-lightning-free",
102
+ max_tokens: 1,
103
+ stream: true,
104
+ messages: [{ role: "user", content: "ping" }],
105
+ };
106
+ ensureChatFingerprintTools(probePayload);
107
+ body = JSON.stringify(probePayload);
108
+ } else {
109
+ fetchUrl = `${cleanUrl}/v1/models`;
110
+ headers["x-relay-target"] = "https://opencode.ai";
111
+ headers["x-relay-path"] = "/zen/v1/models";
112
+ }
113
+ }
114
+
115
+ const res = await fetch(fetchUrl, {
116
+ method,
117
+ headers,
118
+ body,
119
+ signal: AbortSignal.timeout(timeout),
120
+ });
121
+ return { ok: res.ok, status: res.status, latencyMs: Date.now() - start };
122
+ } catch (e) {
123
+ return {
124
+ ok: false,
125
+ status: 0,
126
+ latencyMs: Date.now() - start,
127
+ error: (e as Error)?.message || String(e),
128
+ };
129
+ }
130
+ }
131
+
132
+ export async function probeRelay(
133
+ url: string,
134
+ auth?: string,
135
+ opts?: RelayProbeOptions,
136
+ ): Promise<RelayProbeResult> {
137
+ const retries = Math.max(0, opts?.retries ?? 0);
138
+ const retryDelay = Math.max(10, opts?.retryDelayMs ?? 2000);
139
+
140
+ for (let attempt = 0; attempt <= retries; attempt++) {
141
+ if (attempt > 0) {
142
+ const { promise, resolve } = Promise.withResolvers<void>();
143
+ setTimeout(resolve, retryDelay);
144
+ await promise;
145
+ }
146
+ const result = await probeRelayOnce(url, auth, opts);
147
+ if (result.ok || attempt === retries) {
148
+ return result;
149
+ }
150
+ opts?.onRetry?.(attempt + 1, retries, result);
151
+ }
152
+ return { ok: false, status: 0, latencyMs: 0, error: "Probe failed" };
153
+ }