@inventec/dsh-copilot-auth 1.0.2 → 1.1.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.
@@ -0,0 +1,115 @@
1
+ {
2
+ "anthropic-messages": {
3
+ "claude-fable-5.1": {
4
+ "id": "claude-fable-5.1",
5
+ "name": "Claude Fable 5.1",
6
+ "api": "anthropic-messages",
7
+ "provider": "github-copilot",
8
+ "baseUrl": "https://api.individual.githubcopilot.com",
9
+ "reasoning": true,
10
+ "input": [
11
+ "text",
12
+ "image"
13
+ ],
14
+ "cost": {
15
+ "input": 10,
16
+ "output": 50,
17
+ "cacheRead": 0.25,
18
+ "cacheWrite": 12.5
19
+ },
20
+ "contextWindow": 1000000,
21
+ "maxTokens": 128000,
22
+ "headers": {
23
+ "User-Agent": "GitHubCopilotChat/0.35.0",
24
+ "Editor-Version": "vscode/1.107.0",
25
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
26
+ "Copilot-Integration-Id": "vscode-chat"
27
+ },
28
+ "thinkingLevelMap": {
29
+ "off": null,
30
+ "xhigh": "xhigh",
31
+ "max": "max"
32
+ },
33
+ "compat": {
34
+ "forceAdaptiveThinking": true
35
+ }
36
+ }
37
+ },
38
+ "openai-completions": {
39
+ "gemini-3.8-flash": {
40
+ "id": "gemini-3.8-flash",
41
+ "name": "Gemini 3.8 Flash",
42
+ "api": "openai-completions",
43
+ "provider": "github-copilot",
44
+ "baseUrl": "https://api.individual.githubcopilot.com",
45
+ "reasoning": true,
46
+ "input": [
47
+ "text",
48
+ "image"
49
+ ],
50
+ "cost": {
51
+ "input": 0.75,
52
+ "output": 3.75,
53
+ "cacheRead": 0.075,
54
+ "cacheWrite": 0
55
+ },
56
+ "contextWindow": 1000000,
57
+ "maxTokens": 64000,
58
+ "headers": {
59
+ "User-Agent": "GitHubCopilotChat/0.35.0",
60
+ "Editor-Version": "vscode/1.107.0",
61
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
62
+ "Copilot-Integration-Id": "vscode-chat"
63
+ },
64
+ "compat": {
65
+ "supportsStore": false,
66
+ "supportsDeveloperRole": false,
67
+ "supportsReasoningEffort": false
68
+ }
69
+ },
70
+ "gpt-6-astra": {
71
+ "id": "gpt-6-astra",
72
+ "name": "GPT-6 Astra",
73
+ "api": "openai-completions",
74
+ "provider": "github-copilot",
75
+ "baseUrl": "https://api.individual.githubcopilot.com",
76
+ "reasoning": true,
77
+ "input": [
78
+ "text",
79
+ "image"
80
+ ],
81
+ "cost": {
82
+ "input": 10,
83
+ "output": 50,
84
+ "cacheRead": 1,
85
+ "cacheWrite": 12.5,
86
+ "tiers": [
87
+ {
88
+ "inputTokensAbove": 272000,
89
+ "input": 20,
90
+ "output": 75,
91
+ "cacheRead": 2,
92
+ "cacheWrite": 25
93
+ }
94
+ ]
95
+ },
96
+ "contextWindow": 1050000,
97
+ "maxTokens": 128000,
98
+ "headers": {
99
+ "User-Agent": "GitHubCopilotChat/0.35.0",
100
+ "Editor-Version": "vscode/1.107.0",
101
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
102
+ "Copilot-Integration-Id": "vscode-chat"
103
+ },
104
+ "compat": {
105
+ "supportsStore": false,
106
+ "supportsDeveloperRole": false,
107
+ "supportsReasoningEffort": false
108
+ },
109
+ "thinkingLevelMap": {
110
+ "xhigh": "xhigh",
111
+ "max": "max"
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,156 @@
1
+ // catalog.mjs — 数据核心:tgz 严格解析、条目校验、只增不更新合并、diff、digest、
2
+ // pi-ai 安装定位。合并语义「只增不更新」(ADR 0001):已有 id 的元数据永不覆盖。
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { gunzipSync } from "node:zlib";
7
+
8
+ // github-copilot 目录的三种 wire 协议(白名单,目录外协议拒绝)。
9
+ export const SUPPORTED_APIS = ["anthropic-messages", "openai-completions", "openai-responses"];
10
+
11
+ const TGZ_ENTRY_PATH = "package/dist/providers/data/github-copilot.json";
12
+
13
+ // 严格 ustar 解析:checksum 逐块校验、尺寸越界即截断、目录/pax 条目跳过内容、
14
+ // 其余类型拒绝。返回 [{ name, type, content }]。
15
+ function parseTar(tar) {
16
+ const entries = [];
17
+ let offset = 0;
18
+ while (offset + 512 <= tar.length) {
19
+ const header = tar.subarray(offset, offset + 512);
20
+ if (header.every((b) => b === 0)) break; // 结束零块
21
+ const magic = header.subarray(257, 262).toString("latin1");
22
+ if (magic !== "ustar") throw new Error(`not a ustar archive (magic=${JSON.stringify(magic)})`);
23
+ const stored = Number.parseInt(header.subarray(148, 156).toString("latin1").trim() || "0", 8);
24
+ let sum = 0;
25
+ for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : header[i];
26
+ if (sum !== stored) throw new Error("tar checksum mismatch");
27
+ const name = header.subarray(0, 100).toString("latin1").replace(/\0.*$/s, "");
28
+ const sizeField = header.subarray(124, 136).toString("latin1").replace(/\0.*$/s, "").trim();
29
+ const size = Number.parseInt(sizeField || "0", 8);
30
+ if (!Number.isFinite(size) || size < 0) throw new Error(`bad tar size field for ${name}`);
31
+ const type = String.fromCharCode(header[156]);
32
+ offset += 512;
33
+ if (offset + size > tar.length) throw new Error(`truncated tar (entry ${name})`);
34
+ const content = tar.subarray(offset, offset + size);
35
+ offset += Math.ceil(size / 512) * 512;
36
+ if (type === "0" || type === "\0") entries.push({ name, type, content });
37
+ else if (type === "5" || type === "x" || type === "g") continue; // 目录 / pax:跳过
38
+ else throw new Error(`unsupported tar entry type ${JSON.stringify(type)} for ${name}`);
39
+ }
40
+ return entries;
41
+ }
42
+
43
+ // 从 pi-ai tgz 中精确提取 github-copilot.json。路径精确匹配(前缀不误判)、
44
+ // 重复条目抛错;maxBytes 经 gunzipSync maxOutputLength 落地(防 gzip bomb)。
45
+ export function extractTgzEntry(buf, { maxBytes } = {}) {
46
+ let tar;
47
+ try {
48
+ tar = gunzipSync(buf, maxBytes ? { maxOutputLength: maxBytes } : {});
49
+ } catch (err) {
50
+ if (err instanceof RangeError || /maxOutputLength|too large/i.test(String(err?.message))) {
51
+ throw new Error(`catalog archive too large (decompressed cap ${maxBytes} bytes)`);
52
+ }
53
+ throw err;
54
+ }
55
+ const matches = parseTar(tar).filter((e) => e.name === TGZ_ENTRY_PATH);
56
+ if (matches.length === 0) throw new Error(`entry not found: ${TGZ_ENTRY_PATH}`);
57
+ if (matches.length > 1) throw new Error(`duplicate entry: ${TGZ_ENTRY_PATH}`);
58
+ return matches[0].content;
59
+ }
60
+
61
+ // 单条目校验:true 合法;否则返回原因字符串。唯一调用点是 mergeCatalog(Y2-7)。
62
+ export function validateCatalog(api, id, entry) {
63
+ if (!SUPPORTED_APIS.includes(api)) return `unsupported api: ${api}`;
64
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return "entry is not an object";
65
+ if (entry.id !== id) return `id mismatch: key=${id} entry.id=${entry.id}`;
66
+ if (entry.api !== api) return `api mismatch: section=${api} entry.api=${entry.api}`;
67
+ if (entry.provider !== "github-copilot") return `unexpected provider: ${entry.provider}`;
68
+ for (const field of ["contextWindow", "maxTokens"]) {
69
+ if (typeof entry[field] !== "number" || !Number.isFinite(entry[field]) || entry[field] <= 0) {
70
+ return `${field} must be a positive number`;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+
76
+ // 只增不更新合并。added = 展示数组 [{id, api}];addedOverlay = catalog-shaped
77
+ // 增量(供 journal/自愈,仅含本次合法新增);skipped = 非法条目 [{api, id, reason}];
78
+ // 已有 id(任一层)静默跳过——不覆盖、不进 added/skipped。
79
+ export function mergeCatalog(local, remote) {
80
+ const merged = {};
81
+ for (const [api, section] of Object.entries(local ?? {})) merged[api] = { ...(section ?? {}) };
82
+ const have = new Set(Object.values(merged).flatMap((s) => Object.keys(s)));
83
+ const added = [];
84
+ const skipped = [];
85
+ const addedOverlay = {};
86
+ for (const [api, section] of Object.entries(remote ?? {})) {
87
+ for (const [id, entry] of Object.entries(section ?? {})) {
88
+ if (have.has(id)) continue; // 只增不更新
89
+ const why = validateCatalog(api, id, entry);
90
+ if (why !== true) {
91
+ skipped.push({ api, id, reason: why });
92
+ continue;
93
+ }
94
+ (merged[api] ??= {})[id] = entry;
95
+ (addedOverlay[api] ??= {})[id] = entry;
96
+ added.push({ id, api });
97
+ have.add(id);
98
+ }
99
+ }
100
+ return { merged, added, addedOverlay, skipped };
101
+ }
102
+
103
+ // R3:removed 以 target 为准,不以 available 为准——available 但目录不可解析的
104
+ // 当前模型必须进 removed(settings 校验拒绝目录外 id,保留必报错)。
105
+ export function diffModels(currentIds, availableIds, catalogIds) {
106
+ const target = [...new Set(availableIds)].filter((id) => catalogIds.has(id));
107
+ const targetSet = new Set(target);
108
+ const current = [...new Set(currentIds)];
109
+ return {
110
+ target,
111
+ added: target.filter((id) => !current.includes(id)),
112
+ removed: current.filter((id) => !targetSet.has(id)),
113
+ kept: current.filter((id) => targetSet.has(id)),
114
+ };
115
+ }
116
+
117
+ // canonical JSON:递归键排序;保留 undefined 与缺失键、空数组与空对象的区别
118
+ // (字段级定制变化必须使 settings digest 漂移,R3-2)。
119
+ function canonicalize(v) {
120
+ if (v === undefined) return "undefined";
121
+ if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "undefined";
122
+ if (Array.isArray(v)) return `[${v.map(canonicalize).join(",")}]`;
123
+ return `{${Object.keys(v)
124
+ .sort()
125
+ .map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`)
126
+ .join(",")}}`;
127
+ }
128
+
129
+ export function digest(x) {
130
+ const h = createHash("sha256");
131
+ if (Buffer.isBuffer(x)) h.update(x);
132
+ else if (typeof x === "string") h.update(x);
133
+ else h.update(canonicalize(x), "utf8");
134
+ return h.digest("hex");
135
+ }
136
+
137
+ // 从进程入口(或注入的 startPath)逐级向上定位 pi-ai 安装根;catalog 文件与
138
+ // version 取自同一解析根的 package.json(防版本与被写文件不属同一副本,R3-8)。
139
+ export function findPiAiInstallation({ startPath } = {}) {
140
+ try {
141
+ let dir = startPath ?? dirname(realpathSync(process.argv?.[1] ?? ""));
142
+ for (let depth = 0; depth < 8; depth++) {
143
+ const root = join(dir, "node_modules", "@earendil-works", "pi-ai");
144
+ const catalogFile = join(root, "dist", "providers", "data", "github-copilot.json");
145
+ const packageJsonFile = join(root, "package.json");
146
+ if (existsSync(catalogFile) && existsSync(packageJsonFile)) {
147
+ const pkg = JSON.parse(readFileSync(packageJsonFile, "utf8"));
148
+ return { catalogFile, packageJsonFile, version: pkg.version };
149
+ }
150
+ const parent = dirname(dir);
151
+ if (parent === dir) return null;
152
+ dir = parent;
153
+ }
154
+ } catch { /* 安装树结构变化 → null(调用方降级) */ }
155
+ return null;
156
+ }
@@ -0,0 +1,113 @@
1
+ // copilot-models.mjs — GitHub Copilot 可用性适配(只读 GET /models)。
2
+ // ⚠️ 显式耦合 pi-ai 0.84.4:COPILOT_HEADERS / COPILOT_API_VERSION / normalizeDomain /
3
+ // getGitHubCopilotBaseUrl / parseGitHubCopilotModelCatalog 的语义逐行抄录自
4
+ // @earendil-works/pi-ai@0.84.4 dist/auth/oauth/github-copilot.js。
5
+ // 不实现 token 获取/刷新/policy 修改;语义漂移或任何失败一律由调用方回退凭证缓存
6
+ // (payload.availableModelIds)。pi-ai 升级后本文件必须对照新版源码复核。
7
+
8
+ // ↓↓↓ 以下常量抄录自 pi-ai 0.84.4 dist/auth/oauth/github-copilot.js(勿改值)
9
+ const COPILOT_HEADERS = {
10
+ "User-Agent": "GitHubCopilotChat/0.35.0",
11
+ "Editor-Version": "vscode/1.107.0",
12
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
13
+ "Copilot-Integration-Id": "vscode-chat",
14
+ };
15
+ const COPILOT_API_VERSION = "2026-06-01";
16
+ // ↑↑↑ 抄录结束
17
+
18
+ const INDIVIDUAL_DEFAULT_BASE_URL = "https://api.individual.githubcopilot.com";
19
+
20
+ // 抄录语义:trim → 空 → null;含 :// 直接 new URL,否则补 https://;取 hostname;异常 → null
21
+ function normalizeDomain(input) {
22
+ const trimmed = String(input ?? "").trim();
23
+ if (!trimmed) return null;
24
+ try {
25
+ const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
26
+ return url.hostname;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // 抄录语义:token 形如 tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
33
+ // proxy.xxx → api.xxx
34
+ function getBaseUrlFromToken(token) {
35
+ const match = String(token ?? "").match(/proxy-ep=([^;]+)/);
36
+ if (!match) return null;
37
+ const proxyHost = match[1];
38
+ const apiHost = proxyHost.replace(/^proxy\./, "api.");
39
+ return `https://${apiHost}`;
40
+ }
41
+
42
+ // 抄录语义:token proxy-ep 优先 → enterprise copilot-api.<domain> → individual 缺省
43
+ export function copilotBaseUrl(access, enterpriseUrl) {
44
+ const enterpriseDomain = normalizeDomain(enterpriseUrl);
45
+ if (access) {
46
+ const urlFromToken = getBaseUrlFromToken(access);
47
+ if (urlFromToken) return urlFromToken;
48
+ }
49
+ if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;
50
+ return INDIVIDUAL_DEFAULT_BASE_URL;
51
+ }
52
+
53
+ function asRecord(value) {
54
+ return value && typeof value === "object" ? value : undefined;
55
+ }
56
+
57
+ // 抄录语义(仅 availableModelIds 部分;policyModelIds 涉及 policy 修改,本适配不实现):
58
+ // tool_calls === false 前置排除;pickerEnabled === true && policyState !== "disabled"
59
+ // 为主集;仅当 individual 端点且主集为空时回退 policyState === "enabled"。
60
+ function parseAvailableModelIds(raw, allowPolicyFallback) {
61
+ const data = asRecord(raw)?.data;
62
+ if (!Array.isArray(data)) {
63
+ throw new Error("Invalid Copilot models response");
64
+ }
65
+ const accountModels = data.flatMap((rawItem) => {
66
+ const item = asRecord(rawItem);
67
+ const id = item?.id;
68
+ if (!item || typeof id !== "string") return [];
69
+ const capabilities = asRecord(item.capabilities);
70
+ const supports = asRecord(capabilities?.supports);
71
+ if (supports?.tool_calls === false) return [];
72
+ return [
73
+ {
74
+ id,
75
+ pickerEnabled: item.model_picker_enabled === true,
76
+ policyState: asRecord(item.policy)?.state,
77
+ },
78
+ ];
79
+ });
80
+ const pickerModelIds = accountModels
81
+ .filter((model) => model.pickerEnabled && model.policyState !== "disabled")
82
+ .map((model) => model.id);
83
+ return pickerModelIds.length > 0 || !allowPolicyFallback
84
+ ? pickerModelIds
85
+ : accountModels.filter((model) => model.policyState === "enabled").map((model) => model.id);
86
+ }
87
+
88
+ // 现场拉取账号可用模型。credential 为 GrantRecord payload:
89
+ // { access, enterpriseUrl?, availableModelIds? }。5s 超时、不重试(对齐 0.84.4
90
+ // token 刷新路径 maxRetries: 0 的行为);任何失败抛错,由调用方回退凭证缓存。
91
+ export async function fetchLiveAvailableModelIds({ credential, fetchImpl } = {}) {
92
+ const access = credential?.access;
93
+ if (!access || typeof fetchImpl !== "function") {
94
+ throw new Error("credential.access and fetchImpl required");
95
+ }
96
+ const baseUrl = copilotBaseUrl(access, credential.enterpriseUrl);
97
+ // 0.84.4 注释:部分 Individual 账号 picker 标志全 false 但 policy 显式 enabled,
98
+ // 回退严格限定在 individual 端点。
99
+ const allowPolicyFallback = baseUrl === INDIVIDUAL_DEFAULT_BASE_URL;
100
+ const response = await fetchImpl(`${baseUrl}/models`, {
101
+ headers: {
102
+ Accept: "application/json",
103
+ Authorization: `Bearer ${access}`,
104
+ ...COPILOT_HEADERS,
105
+ "X-GitHub-Api-Version": COPILOT_API_VERSION,
106
+ },
107
+ signal: AbortSignal.timeout(5000),
108
+ });
109
+ if (!response.ok) {
110
+ throw new Error(`${response.status} ${response.statusText}: GET ${baseUrl}/models`);
111
+ }
112
+ return parseAvailableModelIds(await response.json(), allowPolicyFallback);
113
+ }