@edge-sky/dsh-oauth-adapter 0.1.2-beta.1 → 0.1.2-dev.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/lib/client.js CHANGED
@@ -6,8 +6,37 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/model-types.ts
10
+ const MODEL_PROTOCOLS = {
11
+ "github-copilot": [
12
+ "openai-completions",
13
+ "openai-responses",
14
+ "anthropic-messages"
15
+ ],
16
+ openrouter: ["openai-completions", "anthropic-messages"],
17
+ "openai-codex": ["openai-codex-responses"],
18
+ xai: ["openai-responses"],
19
+ anthropic: ["anthropic-messages"],
20
+ "kimi-coding": ["anthropic-messages"]
21
+ };
22
+ function modelText(value) {
23
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\u0000-\u001f\u007f]/u.test(value);
24
+ }
25
+ /** Validate at the wire and durable-data boundaries; fixed-protocol routes accept no override. */
26
+ function validManual(provider, value) {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
28
+ const m = value;
29
+ if (Object.keys(m).some((k) => ![
30
+ "id",
31
+ "name",
32
+ "api"
33
+ ].includes(k)) || !modelText(m.id)) return false;
34
+ if (m.name !== void 0 && !modelText(m.name)) return false;
35
+ const choices = MODEL_PROTOCOLS[provider];
36
+ return choices.length === 1 ? m.api === void 0 : typeof m.api === "string" && choices.includes(m.api);
37
+ }
38
+ //#endregion
9
39
  //#region src/protocol.ts
10
- /** Shared browser/Host wire vocabulary for the OAuth account surface. */
11
40
  const OAUTH_SOCKET_PATH = "/_edge-sky/dsh-oauth";
12
41
  const OAUTH_SOCKET_PROTOCOL = "dsh-oauth-v1";
13
42
  const PROVIDERS = [
@@ -49,6 +78,262 @@ window.__ModuleLoader__.load({
49
78
  }
50
79
  ];
51
80
  //#endregion
81
+ //#region src/client/OAuthModelsSection.tsx
82
+ /** OAuth-specific model management mounted inside the existing Models settings page. */
83
+ function ModelForm({ provider, initial, busy, t, save, close }) {
84
+ const [id, setId] = (0, react.useState)(initial?.id ?? "");
85
+ const [name, setName] = (0, react.useState)(initial?.name ?? "");
86
+ const choices = MODEL_PROTOCOLS[provider];
87
+ const [api, setApi] = (0, react.useState)(initial?.api ?? choices[0]);
88
+ const model = {
89
+ id: id.trim(),
90
+ ...name.trim() ? { name: name.trim() } : {},
91
+ ...choices.length > 1 ? { api } : {}
92
+ };
93
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
94
+ className: "dsh-oauth-model-form",
95
+ onSubmit: (event) => {
96
+ event.preventDefault();
97
+ if (validManual(provider, model)) save(model);
98
+ },
99
+ children: [
100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelId"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
101
+ required: true,
102
+ maxLength: 256,
103
+ value: id,
104
+ readOnly: initial !== void 0,
105
+ onChange: (event) => setId(event.target.value)
106
+ })] }),
107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
108
+ maxLength: 256,
109
+ value: name,
110
+ onChange: (event) => setName(event.target.value),
111
+ placeholder: id
112
+ })] }),
113
+ choices.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelProtocol"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
114
+ value: api,
115
+ onChange: (event) => setApi(event.target.value),
116
+ children: choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
117
+ value: choice,
118
+ children: choice === "openai-completions" ? "Chat Completions" : choice === "openai-responses" ? "Responses" : choice === "openai-codex-responses" ? "Codex Responses" : "Anthropic Messages"
119
+ }, choice))
120
+ })] }),
121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
122
+ className: "dsh-oauth-muted",
123
+ children: t("manualModelHint")
124
+ }),
125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
126
+ type: "submit",
127
+ disabled: busy || !validManual(provider, model),
128
+ children: t("saveModel")
129
+ }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
131
+ type: "button",
132
+ disabled: busy,
133
+ onClick: close,
134
+ children: t("cancel")
135
+ })
136
+ ]
137
+ });
138
+ }
139
+ function ProviderModels({ page, controller, busy, error, t }) {
140
+ const [editing, setEditing] = (0, react.useState)();
141
+ const [submitted, setSubmitted] = (0, react.useState)(false);
142
+ const [confirmMigration, setConfirmMigration] = (0, react.useState)(false);
143
+ (0, react.useEffect)(() => {
144
+ if (submitted && !busy) {
145
+ if (!error) setEditing(void 0);
146
+ setSubmitted(false);
147
+ }
148
+ }, [
149
+ busy,
150
+ error,
151
+ submitted
152
+ ]);
153
+ const errorCode = error?.code ?? page.errorCode;
154
+ const errorLabel = errorCode === "settings-conflict" ? "modelConflict" : errorCode === "discovery-unsupported" ? "modelUnsupported" : errorCode === "discovery-auth" ? "modelAuthFailed" : errorCode === "discovery-timeout" ? "modelTimedOut" : errorCode === "route-conflict" ? "modelRouteConflict" : "modelOperationFailed";
155
+ const disabled = busy || !page.connected || !page.writable;
156
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
157
+ className: "dsh-oauth-model-provider",
158
+ children: [
159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: PROVIDERS.find((p) => p.id === page.provider)?.modelGroupLabel }),
160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
161
+ page.connected ? t("modelConnected") : t("disconnected"),
162
+ " · ",
163
+ page.source === "account" ? t("accountCatalog") : t("builtinCatalog")
164
+ ] }),
165
+ page.syncedAt && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
166
+ t("lastModelSync"),
167
+ ": ",
168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
169
+ dateTime: page.syncedAt,
170
+ children: new Date(page.syncedAt).toLocaleString()
171
+ })
172
+ ] }),
173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
174
+ t("matchedModels"),
175
+ ": ",
176
+ page.counts.matched,
177
+ " · ",
178
+ t("pendingModels"),
179
+ ": ",
180
+ page.counts.pending,
181
+ " · ",
182
+ t("manualModels"),
183
+ ": ",
184
+ page.counts.manual
185
+ ] }),
186
+ (error || page.error) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
187
+ role: "alert",
188
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t(errorLabel) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("technicalDetails") }), error?.message ?? page.error] })]
189
+ }),
190
+ page.migration ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
191
+ disabled,
192
+ onClick: () => setConfirmMigration(!confirmMigration),
193
+ children: t("reviewMigration")
194
+ }), confirmMigration && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("migrationDescription") }),
196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
197
+ page.migration.fields.join(", "),
198
+ " · ",
199
+ page.migration.models,
200
+ " ",
201
+ t("modelsTitle")
202
+ ] }),
203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
204
+ disabled,
205
+ onClick: () => controller.migrateModels(page.provider),
206
+ children: t("confirmMigration")
207
+ })
208
+ ] })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
209
+ disabled: disabled || !page.managed,
210
+ onClick: () => controller.syncModels(page.provider),
211
+ children: busy ? t("modelWorking") : t("syncModels")
212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
213
+ disabled: disabled || !page.managed,
214
+ onClick: () => setEditing(null),
215
+ children: t("addModel")
216
+ })] }),
217
+ editing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelForm, {
218
+ provider: page.provider,
219
+ ...editing === null ? {} : { initial: editing },
220
+ busy,
221
+ t,
222
+ save: (model) => {
223
+ setSubmitted(true);
224
+ controller.saveModel(page.provider, model);
225
+ },
226
+ close: () => setEditing(void 0)
227
+ }, editing?.id ?? "new"),
228
+ [
229
+ "matched",
230
+ "pending",
231
+ "manual"
232
+ ].map((source) => {
233
+ const rows = page.rows.filter((row) => (row.source === "catalog" ? "matched" : row.source) === source);
234
+ if (!rows.length) return null;
235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
236
+ "aria-label": t(source === "pending" ? "pendingModels" : source === "manual" ? "manualModels" : "matchedModels"),
237
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t(source === "pending" ? "pendingModels" : source === "manual" ? "manualModels" : "matchedModels") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
238
+ className: "dsh-oauth-model-list",
239
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: row.name }),
242
+ " ",
243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: row.id }),
244
+ " ",
245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(row.source === "pending" ? "pendingModels" : row.source === "manual" ? "manualModels" : "matchedModels") })
246
+ ] }),
247
+ row.source === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
248
+ disabled: disabled || !page.managed,
249
+ onClick: () => setEditing({
250
+ id: row.id,
251
+ name: row.name
252
+ }),
253
+ children: t("configureModel")
254
+ }),
255
+ row.source === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
256
+ disabled,
257
+ onClick: () => setEditing({
258
+ id: row.id,
259
+ name: row.name,
260
+ ...MODEL_PROTOCOLS[page.provider].length > 1 ? { api: row.api } : {}
261
+ }),
262
+ children: t("editModel")
263
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
264
+ disabled,
265
+ onClick: () => controller.deleteModel(page.provider, row.id),
266
+ children: t("deleteModel")
267
+ })] })
268
+ ] }, row.id))
269
+ })]
270
+ }, source);
271
+ }),
272
+ page.total === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("noModels") }),
273
+ page.total > 10 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
274
+ "aria-label": t("modelsPagination"),
275
+ children: [
276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
277
+ disabled: page.offset === 0 || busy,
278
+ onClick: () => controller.readModels(page.provider, Math.max(0, page.offset - 10)),
279
+ children: t("previousModels")
280
+ }),
281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
282
+ page.offset + 1,
283
+ "–",
284
+ Math.min(page.offset + 10, page.total),
285
+ " / ",
286
+ page.total
287
+ ] }),
288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
289
+ disabled: page.offset + 10 >= page.total || busy,
290
+ onClick: () => controller.readModels(page.provider, page.offset + 10),
291
+ children: t("nextModels")
292
+ })
293
+ ]
294
+ })
295
+ ]
296
+ });
297
+ }
298
+ function OAuthModelsSection(props) {
299
+ const { controller, t } = props;
300
+ if (!controller || !t) return null;
301
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthModels, {
302
+ controller,
303
+ t
304
+ });
305
+ }
306
+ function OAuthModels({ controller, t }) {
307
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
308
+ (0, react.useEffect)(() => {
309
+ controller.loadModels();
310
+ }, [controller]);
311
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
312
+ className: "dsh-oauth-section",
313
+ children: [
314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("modelsTitle") }),
315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("modelsIntro") }),
316
+ snapshot.connection !== "open" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
317
+ role: "status",
318
+ children: t("hostDisconnected")
319
+ }),
320
+ PROVIDERS.map((provider) => {
321
+ const page = snapshot.modelPages?.[provider.id];
322
+ if (!page) return null;
323
+ if (!page.connected && !page.managed && page.counts.manual === 0) return null;
324
+ const error = snapshot.modelErrors?.[provider.id];
325
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderModels, {
326
+ page,
327
+ controller,
328
+ t,
329
+ busy: snapshot.connection !== "open" || snapshot.modelBusy?.[provider.id] === true || page.busy,
330
+ ...error ? { error } : {}
331
+ }, provider.id);
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ //#endregion
52
337
  //#region src/client/OAuthAccountsSection.tsx
53
338
  /** OAuth account settings page. */
54
339
  function safeHttpUrl(value) {
@@ -179,7 +464,7 @@ window.__ModuleLoader__.load({
179
464
  t: props.t
180
465
  }, props.attempt.prompt.id),
181
466
  props.attempt.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorPanel, {
182
- message: props.attempt.error,
467
+ message: props.attempt.phase === "authorized" ? props.t("loginSyncFailed") : props.attempt.error,
183
468
  ...props.attempt.errorDetail === void 0 ? {} : { detail: props.attempt.errorDetail },
184
469
  t: props.t
185
470
  }),
@@ -297,6 +582,40 @@ window.__ModuleLoader__.load({
297
582
  //#region src/client/locales.ts
298
583
  /** English copy for the OAuth accounts page. */
299
584
  const en = {
585
+ modelUnsupported: "This provider does not support account discovery with the current credential. You can still add models manually.",
586
+ modelAuthFailed: "Model discovery was denied. Reconnect or check this account’s permissions. The previous list was retained.",
587
+ modelTimedOut: "Model synchronization timed out. The previous list was retained.",
588
+ modelRouteConflict: "This provider is owned by an existing configuration. Review the details before migration.",
589
+ loginSyncFailed: "Sign-in succeeded, but model synchronization failed. Open Models to retry.",
590
+ modelsTitle: "OAuth models",
591
+ modelsIntro: "Synchronize account-visible models or add a model manually. Manual models survive synchronization.",
592
+ modelId: "Model ID",
593
+ modelName: "Display name",
594
+ modelProtocol: "Request protocol",
595
+ manualModelHint: "Saving configures a request; it does not verify account access or model capabilities.",
596
+ saveModel: "Save model",
597
+ modelConnected: "Connected",
598
+ accountCatalog: "Account-synchronized list",
599
+ builtinCatalog: "Built-in list · account access unverified",
600
+ lastModelSync: "Last successful synchronization",
601
+ matchedModels: "Matched",
602
+ pendingModels: "Needs configuration",
603
+ manualModels: "Manual",
604
+ modelConflict: "Models changed elsewhere. The list was refreshed; review it and retry.",
605
+ modelOperationFailed: "The model operation failed. Review the details before retrying.",
606
+ reviewMigration: "Review migration",
607
+ migrationDescription: "Move this OAuth provider configuration to plugin management while preserving its ID and credentials. The following configuration fields will be retained:",
608
+ confirmMigration: "Migrate this provider",
609
+ modelWorking: "Working…",
610
+ syncModels: "Synchronize models",
611
+ addModel: "Add model",
612
+ configureModel: "Configure",
613
+ editModel: "Edit",
614
+ deleteModel: "Delete manual entry",
615
+ noModels: "No selectable models. Configure an unmatched model or add one manually.",
616
+ modelsPagination: "OAuth model pages",
617
+ previousModels: "Previous",
618
+ nextModels: "Next",
300
619
  nav: "OAuth Accounts",
301
620
  title: "OAuth Accounts",
302
621
  intro: "Connect subscription-backed providers. Their models appear in separate OAuth groups, apart from API-key providers.",
@@ -329,6 +648,40 @@ window.__ModuleLoader__.load({
329
648
  };
330
649
  /** Simplified Chinese copy for the OAuth accounts page. */
331
650
  const zh = {
651
+ modelUnsupported: "当前凭据不支持查询该供应商的账号模型列表,仍可手动添加模型。",
652
+ modelAuthFailed: "模型查询被拒绝,请重新连接或检查账号权限。已保留之前的列表。",
653
+ modelTimedOut: "模型同步超时,已保留之前的列表。",
654
+ modelRouteConflict: "该供应商由现有配置管理,请查看详情后再迁移。",
655
+ loginSyncFailed: "登录成功,但模型同步失败。请前往模型页面重试。",
656
+ modelsTitle: "OAuth 模型",
657
+ modelsIntro: "同步账号可见模型,或手动添加模型。同步时会保留手动模型。",
658
+ modelId: "模型 ID",
659
+ modelName: "显示名称",
660
+ modelProtocol: "调用协议",
661
+ manualModelHint: "保存后可以尝试调用,但不代表已经验证账号权限或模型能力。",
662
+ saveModel: "保存模型",
663
+ modelConnected: "已连接",
664
+ accountCatalog: "已同步账号列表",
665
+ builtinCatalog: "内置列表 · 尚未验证账号权限",
666
+ lastModelSync: "最近成功同步",
667
+ matchedModels: "已匹配",
668
+ pendingModels: "待配置",
669
+ manualModels: "手动模型",
670
+ modelConflict: "模型已被其他操作修改。列表已刷新,请确认后重试。",
671
+ modelOperationFailed: "模型操作失败,请查看详情后重试。",
672
+ reviewMigration: "预览迁移",
673
+ migrationDescription: "将此 OAuth 供应商配置迁移到插件管理,保留供应商 ID 和凭据。以下配置字段将被保留:",
674
+ confirmMigration: "迁移此供应商",
675
+ modelWorking: "正在处理…",
676
+ syncModels: "同步模型",
677
+ addModel: "手动添加",
678
+ configureModel: "补充配置",
679
+ editModel: "编辑",
680
+ deleteModel: "删除手动项",
681
+ noModels: "暂无可选模型,请补充待配置项或手动添加。",
682
+ modelsPagination: "OAuth 模型分页",
683
+ previousModels: "上一页",
684
+ nextModels: "下一页",
332
685
  nav: "OAuth 账户",
333
686
  title: "OAuth 账户",
334
687
  intro: "连接订阅型模型服务。OAuth 模型会以独立分组显示,与 API 密钥提供商分开。",
@@ -395,6 +748,9 @@ window.__ModuleLoader__.load({
395
748
  reconnectTimer;
396
749
  reconnectDelay = 500;
397
750
  disposed = false;
751
+ modelsVisible = false;
752
+ modelRequests = /* @__PURE__ */ new Map();
753
+ latestModelReads = /* @__PURE__ */ new Map();
398
754
  constructor() {
399
755
  this.connect();
400
756
  }
@@ -453,6 +809,69 @@ window.__ModuleLoader__.load({
453
809
  provider
454
810
  });
455
811
  }
812
+ loadModels() {
813
+ this.modelsVisible = true;
814
+ for (const provider of this.snapshot.accounts) this.readModels(provider.provider);
815
+ }
816
+ readModels(provider, offset = 0) {
817
+ if (this.snapshot.connection !== "open") return;
818
+ const requestId = crypto.randomUUID();
819
+ this.latestModelReads.set(provider, requestId);
820
+ this.modelRequests.set(requestId, {
821
+ provider,
822
+ write: false
823
+ });
824
+ this.send({
825
+ type: "models-list",
826
+ requestId,
827
+ provider,
828
+ offset
829
+ });
830
+ }
831
+ syncModels(provider) {
832
+ this.writeModels(provider, { type: "models-sync" });
833
+ }
834
+ migrateModels(provider) {
835
+ this.writeModels(provider, { type: "models-migrate" });
836
+ }
837
+ saveModel(provider, model) {
838
+ this.writeModels(provider, {
839
+ type: "models-save",
840
+ model
841
+ });
842
+ }
843
+ deleteModel(provider, id) {
844
+ this.writeModels(provider, {
845
+ type: "models-delete",
846
+ id
847
+ });
848
+ }
849
+ writeModels(provider, operation) {
850
+ const page = this.snapshot.modelPages?.[provider];
851
+ if (!page || this.snapshot.modelBusy?.[provider] || this.snapshot.connection !== "open") return;
852
+ const requestId = crypto.randomUUID();
853
+ this.modelRequests.set(requestId, {
854
+ provider,
855
+ write: true
856
+ });
857
+ this.update({
858
+ ...this.snapshot,
859
+ modelBusy: {
860
+ ...this.snapshot.modelBusy,
861
+ [provider]: true
862
+ },
863
+ modelErrors: {
864
+ ...this.snapshot.modelErrors,
865
+ [provider]: void 0
866
+ }
867
+ });
868
+ this.send({
869
+ ...operation,
870
+ requestId,
871
+ provider,
872
+ revision: page.revision
873
+ });
874
+ }
456
875
  retry() {
457
876
  if (this.socket?.readyState === WebSocket.OPEN || this.socket?.readyState === WebSocket.CONNECTING) return;
458
877
  if (this.reconnectTimer !== void 0) window.clearTimeout(this.reconnectTimer);
@@ -483,6 +902,12 @@ window.__ModuleLoader__.load({
483
902
  ...current,
484
903
  connection: "open"
485
904
  });
905
+ this.modelRequests.clear();
906
+ this.latestModelReads.clear();
907
+ this.update({
908
+ ...this.snapshot,
909
+ modelBusy: {}
910
+ });
486
911
  this.send({
487
912
  type: "refresh",
488
913
  requestId: crypto.randomUUID()
@@ -551,6 +976,60 @@ window.__ModuleLoader__.load({
551
976
  }
552
977
  receive(message) {
553
978
  switch (message.type) {
979
+ case "models-changed":
980
+ if (this.modelsVisible) this.readModels(message.provider, this.snapshot.modelPages?.[message.provider]?.offset ?? 0);
981
+ return;
982
+ case "models-page": {
983
+ const request = this.modelRequests.get(message.requestId);
984
+ this.modelRequests.delete(message.requestId);
985
+ if (!request) return;
986
+ if (!request.write && this.latestModelReads.get(request.provider) !== message.requestId) return;
987
+ const current = this.snapshot.modelPages?.[request.provider];
988
+ if (current && message.page.revision < current.revision) {
989
+ if (request.write) this.update({
990
+ ...this.snapshot,
991
+ modelBusy: {
992
+ ...this.snapshot.modelBusy,
993
+ [request.provider]: false
994
+ }
995
+ });
996
+ return;
997
+ }
998
+ this.update({
999
+ ...this.snapshot,
1000
+ modelPages: {
1001
+ ...this.snapshot.modelPages,
1002
+ [request.provider]: message.page
1003
+ },
1004
+ modelBusy: {
1005
+ ...this.snapshot.modelBusy,
1006
+ ...request.write ? { [request.provider]: false } : {}
1007
+ }
1008
+ });
1009
+ return;
1010
+ }
1011
+ case "models-error": {
1012
+ const request = this.modelRequests.get(message.requestId);
1013
+ this.modelRequests.delete(message.requestId);
1014
+ if (!request) return;
1015
+ if (!request.write && this.latestModelReads.get(request.provider) !== message.requestId) return;
1016
+ this.update({
1017
+ ...this.snapshot,
1018
+ modelBusy: {
1019
+ ...this.snapshot.modelBusy,
1020
+ ...request.write ? { [request.provider]: false } : {}
1021
+ },
1022
+ modelErrors: {
1023
+ ...this.snapshot.modelErrors,
1024
+ [request.provider]: {
1025
+ code: message.code,
1026
+ message: message.message
1027
+ }
1028
+ }
1029
+ });
1030
+ if (request.write) this.readModels(request.provider);
1031
+ return;
1032
+ }
554
1033
  case "snapshot":
555
1034
  const accounts = [...message.accounts];
556
1035
  const reconciled = reconcileAttemptView(this.snapshot.attempt, accounts);
@@ -561,6 +1040,7 @@ window.__ModuleLoader__.load({
561
1040
  if (reconciled === void 0) delete nextSnapshot.attempt;
562
1041
  else nextSnapshot.attempt = reconciled;
563
1042
  this.update(nextSnapshot);
1043
+ if (this.modelsVisible) for (const account of accounts) this.readModels(account.provider, this.snapshot.modelPages?.[account.provider]?.offset ?? 0);
564
1044
  return;
565
1045
  case "started":
566
1046
  const { error: _error, errorDetail: _errorDetail, ...current } = this.snapshot;
@@ -671,11 +1151,19 @@ window.__ModuleLoader__.load({
671
1151
  .dsh-oauth-actions{display:flex;gap:6px;margin-left:auto}.dsh-oauth-button{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:15px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}.dsh-oauth-button[data-variant=primary]{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.dsh-oauth-button[data-variant=danger]{border-color:transparent;color:var(--dsw-alias-state-error-primary)}.dsh-oauth-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.dsh-oauth-button[data-variant=primary]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover);color:var(--dsw-alias-label-primary-foreground)}.dsh-oauth-button[data-variant=danger]:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.dsh-oauth-button:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.dsh-oauth-button:disabled{cursor:not-allowed;opacity:.4}
672
1152
  .dsh-oauth-flow{display:flex;flex-direction:column;gap:8px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l3);font-size:13px;line-height:20px}.dsh-oauth-link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);text-decoration:none}.dsh-oauth-link:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.dsh-oauth-link-label{font-weight:500}.dsh-oauth-link-host{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsh-oauth-link-icon{color:var(--dsw-alias-brand-text)}.dsh-oauth-code{display:flex;align-items:center;gap:8px}.dsh-oauth-code code{padding:5px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);font-size:14px;letter-spacing:.08em}
673
1153
  .dsh-oauth-prompt{display:flex;flex-direction:column;gap:7px}.dsh-oauth-input,.dsh-oauth-select{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
1154
+
1155
+ .dsh-oauth-model-provider { border: 1px solid var(--border-color, currentColor); border-radius: 8px; padding: 16px; margin-block: 16px; }
1156
+ .dsh-oauth-model-provider button { margin: 4px; }
1157
+ .dsh-oauth-model-list { padding: 0; list-style: none; }
1158
+ .dsh-oauth-model-list li { padding-block: 8px; border-bottom: 1px solid var(--border-color, currentColor); overflow-wrap: anywhere; }
1159
+ .dsh-oauth-model-list code { margin-inline: 8px; }
1160
+ .dsh-oauth-model-form { display: grid; gap: 12px; margin-block: 12px; }
1161
+ .dsh-oauth-model-form label { display: grid; gap: 4px; }
1162
+ .dsh-oauth-model-provider nav { display: flex; align-items: center; gap: 8px; }
674
1163
  `;
675
1164
  //#endregion
676
1165
  //#region src/client/index.ts
677
1166
  const NS = "settings.oauth-accounts";
678
- const MODEL_SETTINGS_VISIBILITY = "modelSettingsVisibility";
679
1167
  /** Required browser services for settings placement and localized copy. */
680
1168
  const inject = ["slots", "locale"];
681
1169
  /** Register the OAuth account page and its app-lifetime connection controller. */
@@ -697,11 +1185,16 @@ window.__ModuleLoader__.load({
697
1185
  ctx.effect(() => () => {
698
1186
  controller.dispose();
699
1187
  }, "dsh-oauth-adapter: controller");
700
- ctx.inject([MODEL_SETTINGS_VISIBILITY], (scope) => {
701
- const visibility = scope.get(MODEL_SETTINGS_VISIBILITY);
702
- scope.effect(() => visibility.registerHiddenProviders(PROVIDERS.map((provider) => provider.id)), "dsh-oauth-adapter: hide OAuth providers from Models settings");
703
- });
704
1188
  const t = ctx.locale.bind(NS);
1189
+ ctx.slots.inject("settings.models.footer", () => ctx.slots.register({
1190
+ name: "settings.models.footer",
1191
+ id: "oauth-models",
1192
+ order: 10,
1193
+ inject: () => ({
1194
+ controller,
1195
+ t
1196
+ })
1197
+ }, OAuthModelsSection));
705
1198
  ctx.slots.inject("settings.section", () => ctx.slots.register({
706
1199
  name: "settings.section",
707
1200
  id: "oauth-accounts",
package/lib/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * `dsh-llm-pi-ai` plugin.
4
4
  * @module @edge-sky/dsh-oauth-adapter
5
5
  */
6
+ import { type ModelContext } from './model-service.js';
6
7
  import type { IncomingMessage } from 'node:http';
7
- import type { Context } from '@deepseek-ai/cordis';
8
8
  import type { AuthorizationService } from '@deepseek-ai/dsh-authorization';
9
9
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
10
10
  import type { WebServer } from '@deepseek-ai/dsh-host-webserver';
@@ -14,6 +14,10 @@ import z from '@deepseek-ai/schemastery';
14
14
  export interface Config {
15
15
  /** Emit secret-free transport lifecycle diagnostics. */
16
16
  debug?: boolean;
17
+ modelSyncTimeoutMs?: number;
18
+ modelSyncMaxResponseBytes?: number;
19
+ modelSyncMaxPages?: number;
20
+ codexClientVersion?: string;
17
21
  }
18
22
  /** Runtime-validated plugin configuration. */
19
23
  export declare const Config: z<Config>;
@@ -21,7 +25,7 @@ export declare const Config: z<Config>;
21
25
  export declare const name = "dsh-oauth-adapter";
22
26
  /** Official services required by the OAuth account surface. */
23
27
  export declare const inject: string[];
24
- type OAuthContext = Context & {
28
+ type OAuthContext = ModelContext & {
25
29
  authorization: AuthorizationService;
26
30
  credentials: CredentialProvider;
27
31
  settings: SettingsProvider;
@@ -35,7 +39,7 @@ export declare function safeNoticeUrl(value: string | undefined): string | undef
35
39
  export declare function acceptsBrowserUpgrade(request: IncomingMessage): boolean;
36
40
  /** Format one Host failure for browser diagnostics without forwarding credentials or unbounded data. */
37
41
  export declare function formatErrorDetail(error: unknown): string;
38
- /** Mount the loopback-only OAuth WebSocket over official rc2 services. */
42
+ /** Mount the loopback-only OAuth WebSocket over official rc services. */
39
43
  export declare function apply(ctx: OAuthContext, config: Config): () => Promise<void>;
40
44
  export {};
41
45
  //# sourceMappingURL=index.d.ts.map