pi-clinepass 0.1.0 → 0.1.2
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 +32 -0
- package/LICENSE +21 -21
- package/README.md +122 -117
- package/package.json +6 -4
- package/src/auth.ts +375 -363
- package/src/calibrate.ts +773 -0
- package/src/catalog.ts +85 -28
- package/src/errors.ts +114 -112
- package/src/headers.ts +83 -80
- package/src/index.ts +234 -21
- package/src/pricing.ts +204 -0
- package/src/settings.ts +56 -49
- package/src/thinking.ts +104 -0
- package/src/usage.ts +895 -743
- package/src/views.ts +397 -0
- package/src/workos.ts +245 -231
package/src/catalog.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* (verified against real usage), not the published reference table which
|
|
6
6
|
* drifts from actual billing. `cacheWrite` is kept at 0: pi's cost model
|
|
7
7
|
* requires the field, but we do not display or track cache-write pricing.
|
|
8
|
+
*
|
|
9
|
+
* Context is capped to 921,600 for models whose gateway reports ~1M
|
|
10
|
+
* (pool max, not single-node) to leave headroom; max output is capped to
|
|
11
|
+
* 131,072. Models under 1M keep their tested values.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -15,7 +19,7 @@ export interface ClinePassModel {
|
|
|
15
19
|
id: string;
|
|
16
20
|
name: string;
|
|
17
21
|
reasoning: boolean;
|
|
18
|
-
input: readonly
|
|
22
|
+
input: readonly ("text" | "image")[];
|
|
19
23
|
cost: {
|
|
20
24
|
input: number;
|
|
21
25
|
output: number;
|
|
@@ -32,10 +36,10 @@ export interface ClinePassModel {
|
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
/** All levels supported (off → "none")
|
|
36
|
-
const
|
|
39
|
+
/** All 7 levels supported (off → "none"). Used when `supportedEfforts: null` + mandatory false. */
|
|
40
|
+
const ALL_THINKING: ThinkingLevelMap = {
|
|
37
41
|
off: "none",
|
|
38
|
-
minimal:
|
|
42
|
+
minimal: "minimal",
|
|
39
43
|
low: "low",
|
|
40
44
|
medium: "medium",
|
|
41
45
|
high: "high",
|
|
@@ -43,10 +47,59 @@ const FULL_THINKING: ThinkingLevelMap = {
|
|
|
43
47
|
max: "max",
|
|
44
48
|
};
|
|
45
49
|
|
|
46
|
-
/**
|
|
47
|
-
const
|
|
48
|
-
|
|
50
|
+
/** All levels except off (mandatory reasoning). Used when `mandatory: true` + `supportedEfforts: null`. */
|
|
51
|
+
const ALL_MANDATORY: ThinkingLevelMap = {
|
|
52
|
+
off: null,
|
|
53
|
+
minimal: "minimal",
|
|
54
|
+
low: "low",
|
|
55
|
+
medium: "medium",
|
|
56
|
+
high: "high",
|
|
57
|
+
xhigh: "xhigh",
|
|
58
|
+
max: "max",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Only max/high/low + off. Used for deepseek, glm-5.3, kimi-k3, free models. */
|
|
62
|
+
const MAX_HIGH_LOW: ThinkingLevelMap = {
|
|
63
|
+
off: "none",
|
|
64
|
+
minimal: null,
|
|
65
|
+
low: "low",
|
|
66
|
+
medium: null,
|
|
67
|
+
high: "high",
|
|
68
|
+
xhigh: null,
|
|
69
|
+
max: "max",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Max/high/low but off not allowed (mandatory). */
|
|
73
|
+
const MAX_HIGH_LOW_MANDATORY: ThinkingLevelMap = {
|
|
49
74
|
off: null,
|
|
75
|
+
minimal: null,
|
|
76
|
+
low: "low",
|
|
77
|
+
medium: null,
|
|
78
|
+
high: "high",
|
|
79
|
+
xhigh: null,
|
|
80
|
+
max: "max",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Only xhigh/high + off. Used for glm-5.2. */
|
|
84
|
+
const XHIGH_HIGH: ThinkingLevelMap = {
|
|
85
|
+
off: "none",
|
|
86
|
+
minimal: null,
|
|
87
|
+
low: null,
|
|
88
|
+
medium: null,
|
|
89
|
+
high: "high",
|
|
90
|
+
xhigh: "xhigh",
|
|
91
|
+
max: null,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** xhigh/medium/low + off. Used for qwen3.8-max. */
|
|
95
|
+
const XHIGH_MEDIUM_LOW: ThinkingLevelMap = {
|
|
96
|
+
off: "none",
|
|
97
|
+
minimal: null,
|
|
98
|
+
low: "low",
|
|
99
|
+
medium: "medium",
|
|
100
|
+
high: null,
|
|
101
|
+
xhigh: "xhigh",
|
|
102
|
+
max: null,
|
|
50
103
|
};
|
|
51
104
|
|
|
52
105
|
/**
|
|
@@ -70,13 +123,14 @@ function model(
|
|
|
70
123
|
cost: [input: number, output: number, cacheRead: number],
|
|
71
124
|
contextWindow: number,
|
|
72
125
|
maxTokens: number,
|
|
73
|
-
|
|
126
|
+
input: readonly ("text" | "image")[] = ["text"],
|
|
127
|
+
thinkingLevelMap: ThinkingLevelMap = ALL_THINKING,
|
|
74
128
|
): ClinePassModel {
|
|
75
129
|
return {
|
|
76
130
|
id,
|
|
77
131
|
name,
|
|
78
132
|
reasoning: true,
|
|
79
|
-
input
|
|
133
|
+
input,
|
|
80
134
|
cost: { input: cost[0], output: cost[1], cacheRead: cost[2], cacheWrite: 0 },
|
|
81
135
|
contextWindow,
|
|
82
136
|
maxTokens,
|
|
@@ -85,33 +139,36 @@ function model(
|
|
|
85
139
|
};
|
|
86
140
|
}
|
|
87
141
|
|
|
88
|
-
/** Measured prices ($/1M tokens: input/output/cacheRead). */
|
|
142
|
+
/** Measured prices ($/1M tokens: input/output/cacheRead) — latest verified. */
|
|
89
143
|
export const MODELS: readonly ClinePassModel[] = [
|
|
90
144
|
// ── Free models (Cline free tier, cost 0) ──────────────────────────────
|
|
91
|
-
model("
|
|
92
|
-
model("
|
|
93
|
-
model("
|
|
145
|
+
model("cline-free/longcat-2.0", "LongCat 2.0", [0, 0, 0], 921_600, 131_072, ["text"], ALL_THINKING),
|
|
146
|
+
model("z-ai/glm-5.3-flash", "GLM-5.3 Flash", [0, 0, 0], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW_MANDATORY),
|
|
147
|
+
model("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", [0, 0, 0], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW),
|
|
148
|
+
model("poolside/laguna-s-2.1:free", "Laguna S-2.1", [0, 0, 0], 262_144, 131_072, ["text"], ALL_THINKING),
|
|
94
149
|
// ── ClinePass models (measured billing prices) ─────────────────────────
|
|
95
|
-
model("cline-pass/glm-5.3", "GLM-5.3
|
|
96
|
-
model("cline-pass/glm-5.
|
|
97
|
-
model("cline-pass/
|
|
98
|
-
model("cline-pass/kimi-k2.
|
|
99
|
-
model("cline-pass/kimi-
|
|
100
|
-
model("cline-pass/
|
|
101
|
-
model("cline-pass/deepseek-v4-
|
|
102
|
-
model("cline-pass/
|
|
103
|
-
model("cline-pass/mimo-v2.5
|
|
104
|
-
model("cline-pass/
|
|
105
|
-
model("cline-pass/
|
|
106
|
-
model("cline-pass/qwen3.7-
|
|
107
|
-
model("cline-pass/qwen3.
|
|
150
|
+
model("cline-pass/glm-5.3-flash", "GLM-5.3 Flash", [0.15, 0.5, 0.03], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW_MANDATORY),
|
|
151
|
+
model("cline-pass/glm-5.3", "GLM-5.3", [1.4, 4.4, 0.26], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW_MANDATORY),
|
|
152
|
+
model("cline-pass/glm-5.2", "GLM-5.2", [1.4, 4.4, 0.26], 921_600, 131_072, ["text"], XHIGH_HIGH),
|
|
153
|
+
model("cline-pass/kimi-k2.7-code", "Kimi K2.7 Code", [1.58, 6.67, 0.32], 262_144, 131_072, ["text", "image"], ALL_MANDATORY),
|
|
154
|
+
model("cline-pass/kimi-k2.6", "Kimi K2.6", [1.58, 6.67, 0.27], 262_144, 131_072, ["text", "image"], ALL_THINKING),
|
|
155
|
+
model("cline-pass/kimi-k3", "Kimi K3", [6.0, 30.0, 0.6], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW),
|
|
156
|
+
model("cline-pass/deepseek-v4-pro", "DeepSeek V4 Pro", [1.65, 4.95, 0.06], 921_600, 131_072, ["text"], MAX_HIGH_LOW),
|
|
157
|
+
model("cline-pass/deepseek-v4-flash", "DeepSeek V4 Flash", [0.44, 1.32, 0.014], 921_600, 131_072, ["text", "image"], MAX_HIGH_LOW),
|
|
158
|
+
model("cline-pass/mimo-v2.5", "MiMo-V2.5", [0.14, 0.28, 0.0028], 921_600, 131_072, ["text"], ALL_THINKING),
|
|
159
|
+
model("cline-pass/mimo-v2.5-pro", "MiMo-V2.5-Pro", [0.435, 0.87, 0.0036], 921_600, 131_072, ["text"], ALL_THINKING),
|
|
160
|
+
model("cline-pass/minimax-m3", "MiniMax M3", [0.5, 2.0, 0.1], 921_600, 131_072, ["text", "image"], ALL_THINKING),
|
|
161
|
+
model("cline-pass/qwen3.7-plus", "Qwen3.7 Plus", [0.67, 2.67, 0.07], 921_600, 131_072, ["text", "image"], ALL_THINKING),
|
|
162
|
+
model("cline-pass/qwen3.7-max", "Qwen3.7 Max", [4.17, 12.5, 0.83], 921_600, 131_072, ["text"], ALL_THINKING),
|
|
163
|
+
model("cline-pass/qwen3.8-max", "Qwen3.8 Max", [2.75, 8.25, 0.34], 921_600, 131_072, ["text"], XHIGH_MEDIUM_LOW),
|
|
108
164
|
];
|
|
109
165
|
|
|
110
166
|
export function modelIds(): string[] {
|
|
111
167
|
return MODELS.map((m) => m.id);
|
|
112
168
|
}
|
|
113
169
|
|
|
114
|
-
/** True for the Cline free-tier models
|
|
170
|
+
/** True for the Cline free-tier models — derived from the catalog itself
|
|
171
|
+
* (cost 0), so adding a free model never needs a second edit here. */
|
|
115
172
|
export function isFreeModel(id: string): boolean {
|
|
116
|
-
return
|
|
173
|
+
return MODELS.some((m) => m.id === id && m.cost.input === 0);
|
|
117
174
|
}
|
package/src/errors.ts
CHANGED
|
@@ -1,112 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClinePass error classification — maps provider error text to friendly,
|
|
3
|
-
* actionable messages surfaced through pi's UI.
|
|
4
|
-
*
|
|
5
|
-
* Classification order matters: the free-model
|
|
6
|
-
* `403 Forbidden` when
|
|
7
|
-
* be reported as a subscription problem, and free-limit errors may arrive
|
|
8
|
-
* wrapped in a generic 403/429 shell.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
export type ClinePassErrorType =
|
|
14
|
-
| "not_subscribed"
|
|
15
|
-
| "auth_expired"
|
|
16
|
-
| "rate_limited"
|
|
17
|
-
| "free_limit_reached"
|
|
18
|
-
| "free_route_forbidden"
|
|
19
|
-
| "unknown";
|
|
20
|
-
|
|
21
|
-
function matchesAny(text: string, patterns: string[]): boolean {
|
|
22
|
-
return patterns.some((p) => text.includes(p));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const CLINEPASS_ERROR_MESSAGES: Record<ClinePassErrorType, string> = {
|
|
26
|
-
not_subscribed:
|
|
27
|
-
"ClinePass subscription required — or the organization account cannot use ClinePass. " +
|
|
28
|
-
"Visit app.cline.bot to subscribe / switch to your personal account, or run `pi /login`.",
|
|
29
|
-
auth_expired:
|
|
30
|
-
"ClinePass authentication expired. Run `pi /login` and select ClinePass to refresh credentials.",
|
|
31
|
-
rate_limited:
|
|
32
|
-
"ClinePass rate limit reached. Wait a moment and try again, or check your plan at app.cline.bot.",
|
|
33
|
-
free_limit_reached:
|
|
34
|
-
"Free model rate limit reached. Please wait a few moments and try again.",
|
|
35
|
-
free_route_forbidden:
|
|
36
|
-
"Free model route unavailable (HTTP 403). The free
|
|
37
|
-
"surfaces — retry in a moment, or switch to a ClinePass model.",
|
|
38
|
-
unknown: "ClinePass request failed. Check your subscription at app.cline.bot or run `pi /login`.",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Classify a provider error string. `modelId` (the model of the failed
|
|
43
|
-
* request, e.g. "deepseek/deepseek-v4-flash") disambiguates 403s on
|
|
44
|
-
*
|
|
45
|
-
*/
|
|
46
|
-
export function classifyClinePassError(
|
|
47
|
-
errorMessage: string,
|
|
48
|
-
modelId?: string,
|
|
49
|
-
): {
|
|
50
|
-
type: ClinePassErrorType;
|
|
51
|
-
message: string;
|
|
52
|
-
} {
|
|
53
|
-
const lower = errorMessage.toLowerCase();
|
|
54
|
-
|
|
55
|
-
if (matchesAny(lower, ["401", "unauthorized", "invalid api key", "invalid_api_key"])) {
|
|
56
|
-
return { type: "auth_expired", message: CLINEPASS_ERROR_MESSAGES.auth_expired };
|
|
57
|
-
}
|
|
58
|
-
if (matchesAny(lower, ["429", "rate limit", "too many requests", "rate_limit"])) {
|
|
59
|
-
return { type: "rate_limited", message: CLINEPASS_ERROR_MESSAGES.rate_limited };
|
|
60
|
-
}
|
|
61
|
-
if (matchesAny(lower, ["free limit reached", "free limit", "try again in"])) {
|
|
62
|
-
return { type: "free_limit_reached", message: CLINEPASS_ERROR_MESSAGES.free_limit_reached };
|
|
63
|
-
}
|
|
64
|
-
if (
|
|
65
|
-
matchesAny(lower, [
|
|
66
|
-
"403",
|
|
67
|
-
"forbidden",
|
|
68
|
-
"subscription required",
|
|
69
|
-
"not subscribed",
|
|
70
|
-
"organization accounts cannot use",
|
|
71
|
-
])
|
|
72
|
-
) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ClinePass error classification — maps provider error text to friendly,
|
|
3
|
+
* actionable messages surfaced through pi's UI.
|
|
4
|
+
*
|
|
5
|
+
* Classification order matters: the free-model routes return plain
|
|
6
|
+
* `403 Forbidden` when their Cline-CLI header gate rejects us, which must not
|
|
7
|
+
* be reported as a subscription problem, and free-limit errors may arrive
|
|
8
|
+
* wrapped in a generic 403/429 shell.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isFreeModel } from "./catalog.js";
|
|
12
|
+
|
|
13
|
+
export type ClinePassErrorType =
|
|
14
|
+
| "not_subscribed"
|
|
15
|
+
| "auth_expired"
|
|
16
|
+
| "rate_limited"
|
|
17
|
+
| "free_limit_reached"
|
|
18
|
+
| "free_route_forbidden"
|
|
19
|
+
| "unknown";
|
|
20
|
+
|
|
21
|
+
function matchesAny(text: string, patterns: string[]): boolean {
|
|
22
|
+
return patterns.some((p) => text.includes(p));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CLINEPASS_ERROR_MESSAGES: Record<ClinePassErrorType, string> = {
|
|
26
|
+
not_subscribed:
|
|
27
|
+
"ClinePass subscription required — or the organization account cannot use ClinePass. " +
|
|
28
|
+
"Visit app.cline.bot to subscribe / switch to your personal account, or run `pi /login`.",
|
|
29
|
+
auth_expired:
|
|
30
|
+
"ClinePass authentication expired. Run `pi /login` and select ClinePass to refresh credentials.",
|
|
31
|
+
rate_limited:
|
|
32
|
+
"ClinePass rate limit reached. Wait a moment and try again, or check your plan at app.cline.bot.",
|
|
33
|
+
free_limit_reached:
|
|
34
|
+
"Free model rate limit reached. Please wait a few moments and try again.",
|
|
35
|
+
free_route_forbidden:
|
|
36
|
+
"Free model route unavailable (HTTP 403). The free route is gated to Cline product " +
|
|
37
|
+
"surfaces — retry in a moment, or switch to a ClinePass model.",
|
|
38
|
+
unknown: "ClinePass request failed. Check your subscription at app.cline.bot or run `pi /login`.",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify a provider error string. `modelId` (the model of the failed
|
|
43
|
+
* request, e.g. "deepseek/deepseek-v4-flash") disambiguates 403s on free
|
|
44
|
+
* routes from subscription problems.
|
|
45
|
+
*/
|
|
46
|
+
export function classifyClinePassError(
|
|
47
|
+
errorMessage: string,
|
|
48
|
+
modelId?: string,
|
|
49
|
+
): {
|
|
50
|
+
type: ClinePassErrorType;
|
|
51
|
+
message: string;
|
|
52
|
+
} {
|
|
53
|
+
const lower = errorMessage.toLowerCase();
|
|
54
|
+
|
|
55
|
+
if (matchesAny(lower, ["401", "unauthorized", "invalid api key", "invalid_api_key"])) {
|
|
56
|
+
return { type: "auth_expired", message: CLINEPASS_ERROR_MESSAGES.auth_expired };
|
|
57
|
+
}
|
|
58
|
+
if (matchesAny(lower, ["429", "rate limit", "too many requests", "rate_limit"])) {
|
|
59
|
+
return { type: "rate_limited", message: CLINEPASS_ERROR_MESSAGES.rate_limited };
|
|
60
|
+
}
|
|
61
|
+
if (matchesAny(lower, ["free limit reached", "free limit", "try again in"])) {
|
|
62
|
+
return { type: "free_limit_reached", message: CLINEPASS_ERROR_MESSAGES.free_limit_reached };
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
matchesAny(lower, [
|
|
66
|
+
"403",
|
|
67
|
+
"forbidden",
|
|
68
|
+
"subscription required",
|
|
69
|
+
"not subscribed",
|
|
70
|
+
"organization accounts cannot use",
|
|
71
|
+
])
|
|
72
|
+
) {
|
|
73
|
+
// Every free-tier route is gated to Cline product surfaces — a 403 on a
|
|
74
|
+
// free model is a route-gate rejection, not a subscription problem.
|
|
75
|
+
if (modelId && isFreeModel(modelId)) {
|
|
76
|
+
return { type: "free_route_forbidden", message: CLINEPASS_ERROR_MESSAGES.free_route_forbidden };
|
|
77
|
+
}
|
|
78
|
+
return { type: "not_subscribed", message: CLINEPASS_ERROR_MESSAGES.not_subscribed };
|
|
79
|
+
}
|
|
80
|
+
return { type: "unknown", message: CLINEPASS_ERROR_MESSAGES.unknown };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ErrorContext {
|
|
84
|
+
hasUI: boolean;
|
|
85
|
+
ui: {
|
|
86
|
+
notify: (msg: string, type: "info" | "warning" | "error") => void;
|
|
87
|
+
};
|
|
88
|
+
model?: { provider?: string };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Handle a message_end event: classify ClinePass errors and notify. */
|
|
92
|
+
export function handleClinePassError(
|
|
93
|
+
event: { message: unknown },
|
|
94
|
+
ctx: ErrorContext,
|
|
95
|
+
): void {
|
|
96
|
+
if (!event.message) return;
|
|
97
|
+
const msg = event.message as {
|
|
98
|
+
stopReason?: string;
|
|
99
|
+
errorMessage?: string;
|
|
100
|
+
provider?: string;
|
|
101
|
+
model?: string;
|
|
102
|
+
};
|
|
103
|
+
if (msg.stopReason !== "error" || !msg.errorMessage) return;
|
|
104
|
+
|
|
105
|
+
const provider = msg.provider ?? ctx.model?.provider;
|
|
106
|
+
if (provider !== "clinepass" && provider !== "cline-pass") return;
|
|
107
|
+
|
|
108
|
+
const { message: friendly } = classifyClinePassError(msg.errorMessage, msg.model);
|
|
109
|
+
if (ctx.hasUI) {
|
|
110
|
+
ctx.ui.notify(friendly, "error");
|
|
111
|
+
} else {
|
|
112
|
+
console.error(`[pi-clinepass] ${friendly}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/headers.ts
CHANGED
|
@@ -1,80 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Free-model request headers.
|
|
3
|
-
*
|
|
4
|
-
* The gateway gates
|
|
5
|
-
* "Cline product surfaces" (HTTP 403 without them). We identify as the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"x-
|
|
77
|
-
"x-
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Free-model request headers.
|
|
3
|
+
*
|
|
4
|
+
* The gateway gates Cline's free routes (":free" / cost-0 models) behind
|
|
5
|
+
* "Cline product surfaces" (HTTP 403 without them). We identify as the Cline
|
|
6
|
+
* CLI with a current version fetched from the npm registry, cached for 24h
|
|
7
|
+
* with a bundled fallback. Every free-tier model goes through this gate, so
|
|
8
|
+
* the identifying headers are applied to all of them via `isFreeModel`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { release } from "node:os";
|
|
12
|
+
import { isFreeModel } from "./catalog.js";
|
|
13
|
+
|
|
14
|
+
const FALLBACK_CLINE_VERSION = "3.0.60";
|
|
15
|
+
const VERSION_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/cline/latest";
|
|
17
|
+
|
|
18
|
+
/** Header-safe version shape (semver-ish tokens only): a malformed registry
|
|
19
|
+
* response must never reach request headers, where control chars would make
|
|
20
|
+
* undici throw and break the request. */
|
|
21
|
+
const VERSION_PATTERN = /^[\w.\-+]+$/;
|
|
22
|
+
|
|
23
|
+
let cachedVersion: string | undefined;
|
|
24
|
+
let cachedAt = 0;
|
|
25
|
+
let inflightVersion: Promise<string> | undefined;
|
|
26
|
+
|
|
27
|
+
export async function getClineVersion(
|
|
28
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
if (cachedVersion && Date.now() - cachedAt < VERSION_CACHE_TTL_MS) return cachedVersion;
|
|
31
|
+
// Deduplicate concurrent callers (e.g. session_start + a command racing).
|
|
32
|
+
if (inflightVersion) return inflightVersion;
|
|
33
|
+
inflightVersion = (async () => {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetchFn(NPM_REGISTRY_URL);
|
|
36
|
+
if (res.ok) {
|
|
37
|
+
const data = (await res.json()) as { version?: unknown };
|
|
38
|
+
if (typeof data.version === "string" && VERSION_PATTERN.test(data.version)) {
|
|
39
|
+
cachedVersion = data.version;
|
|
40
|
+
cachedAt = Date.now();
|
|
41
|
+
return cachedVersion;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// fall through to bundled default
|
|
46
|
+
}
|
|
47
|
+
// Registry unreachable: use the bundled fallback but do not cache it
|
|
48
|
+
// for the full TTL — the next session retries so a transient outage
|
|
49
|
+
// doesn't pin an outdated version for a day.
|
|
50
|
+
cachedVersion = FALLBACK_CLINE_VERSION;
|
|
51
|
+
cachedAt = 0;
|
|
52
|
+
return cachedVersion;
|
|
53
|
+
})();
|
|
54
|
+
try {
|
|
55
|
+
return await inflightVersion;
|
|
56
|
+
} finally {
|
|
57
|
+
inflightVersion = undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether the given model id needs the Cline-CLI identifying headers (all
|
|
62
|
+
* free-tier models do — the free route is gated to Cline product surfaces). */
|
|
63
|
+
export function needsFreeModelHeaders(modelId: string): boolean {
|
|
64
|
+
return isFreeModel(modelId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the headers that make the free deepseek route servable.
|
|
69
|
+
* Synchronous: uses the cached version (pre-warmed at startup or from a
|
|
70
|
+
* previous request), falling back to the bundled default. Never fetches
|
|
71
|
+
* inside a request path.
|
|
72
|
+
*/
|
|
73
|
+
export function buildFreeModelHeadersSync(): Record<string, string> {
|
|
74
|
+
const version = cachedVersion ?? FALLBACK_CLINE_VERSION;
|
|
75
|
+
return {
|
|
76
|
+
"x-client-type": "cli",
|
|
77
|
+
"x-client-version": version,
|
|
78
|
+
"x-core-version": version,
|
|
79
|
+
"x-platform": process.platform,
|
|
80
|
+
"x-platform-version": release(),
|
|
81
|
+
"user-agent": `Cline/${version}`,
|
|
82
|
+
};
|
|
83
|
+
}
|