@gatemux/dsh-gatemux-login 1.0.2 → 1.0.4
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 +29 -5
- package/account.js +22 -0
- package/llm.js +158 -8
- package/package.json +18 -15
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ composed profile:
|
|
|
12
12
|
- Adds a GateMux account service that signs you in through GateMux's OAuth 2.0
|
|
13
13
|
redirect flow (authorization code with PKCE `S256`).
|
|
14
14
|
- Registers a model route, provider id `gatemux-account`, that authenticates every
|
|
15
|
-
request with your own credential
|
|
15
|
+
request with your own credential and advertises the models the endpoint reports
|
|
16
|
+
it serves.
|
|
16
17
|
- Turns off the shipped vendor account route (`deepseek-account` and
|
|
17
18
|
`llm-deepseek-account`) so there is one account provider instead of two.
|
|
18
19
|
|
|
@@ -22,8 +23,8 @@ keep another default.
|
|
|
22
23
|
|
|
23
24
|
## Requirements
|
|
24
25
|
|
|
25
|
-
- DeepSeek Harness desktop app, `0.1.7.x`.
|
|
26
|
-
The plugin declares `@deepseek-ai/dsh-*` peer dependencies
|
|
26
|
+
- DeepSeek Harness desktop app, `0.1.7.x` or `0.2.x`.
|
|
27
|
+
The plugin declares `@deepseek-ai/dsh-*` peer dependencies across both lines.
|
|
27
28
|
A different DSH release is rejected at install time; installing anyway needs a
|
|
28
29
|
version exemption, which risks crashes and data loss.
|
|
29
30
|
- A GateMux account.
|
|
@@ -70,12 +71,35 @@ never auto-closes; you can close it once the app is signed in.
|
|
|
70
71
|
|
|
71
72
|
## Models
|
|
72
73
|
|
|
74
|
+
Once you are signed in, the route reads what the endpoint serves from
|
|
75
|
+
`https://rest.gatemux.ai/v1/models` and advertises every entry whose `protocols`
|
|
76
|
+
include `anthropic-messages`. An entry served over any other protocol is left
|
|
77
|
+
out, because every request this route makes speaks the Messages protocol: a
|
|
78
|
+
model that speaks only `openai-completions` would fail on every request, so
|
|
79
|
+
offering it in the model selector would be offering a choice that cannot work.
|
|
80
|
+
|
|
81
|
+
Context window, output cap, display name, and image input come from the listing.
|
|
82
|
+
|
|
83
|
+
### Fallback catalog
|
|
84
|
+
|
|
85
|
+
The bundle's patch layer also declares two models, which the route advertises
|
|
86
|
+
before you sign in and keeps when a listing read fails:
|
|
87
|
+
|
|
73
88
|
| Model id | Context | Input |
|
|
74
89
|
|---|---|---|
|
|
75
90
|
| `gatemux/deepseek-v4-flash` | 1,000,000 | text, image |
|
|
76
91
|
| `gatemux/deepseek-v4-pro` | 1,000,000 | text |
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
A failed read is logged as a warning and leaves the last catalog that was read
|
|
94
|
+
successfully in force, so a listing outage never empties the model selector or
|
|
95
|
+
drops the provider from it. Before the first successful read, the declared
|
|
96
|
+
catalog above is what the route advertises.
|
|
97
|
+
|
|
98
|
+
For a model the endpoint also lists, every field the declaration states wins and
|
|
99
|
+
the endpoint fills the rest. `gatemux/deepseek-v4-flash` therefore keeps the
|
|
100
|
+
declared 1,000,000-token context window and image input while taking its output
|
|
101
|
+
cap from the endpoint. A declared model the endpoint does not list is still
|
|
102
|
+
advertised.
|
|
79
103
|
|
|
80
104
|
## Changing the default model
|
|
81
105
|
|
|
@@ -105,7 +129,7 @@ module or the patch is skipped.
|
|
|
105
129
|
| `ACCOUNT_QUOTA_EXCEEDED` | Your GateMux balance or spend cap is exhausted. Top up. |
|
|
106
130
|
| `ip_not_allowed` (HTTP 403) | Your network is not in the GateMux application's trusted egress IP allowlist. Contact the plugin operator. Re-authorizing will not fix it. |
|
|
107
131
|
| `scope_denied`, `model_not_allowed` (HTTP 403) | The application registration does not permit this request. Contact the plugin operator. |
|
|
108
|
-
| Install refused for peer dependencies | Your DSH version is outside `0.1.7.x`. |
|
|
132
|
+
| Install refused for peer dependencies | Your DSH version is outside `0.1.7.x` and `0.2.x`. |
|
|
109
133
|
|
|
110
134
|
403 errors are reported as authorization faults, not retried: they are
|
|
111
135
|
configuration problems on the application's side.
|
package/account.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'
|
|
23
|
+
import { platform, release } from 'node:os'
|
|
23
24
|
import { promises as streamPromises } from 'node:stream'
|
|
24
25
|
|
|
25
26
|
import { Service } from '@deepseek-ai/cordis'
|
|
@@ -393,6 +394,27 @@ export class GateMuxAccount extends DeepSeekAccount {
|
|
|
393
394
|
return null
|
|
394
395
|
}
|
|
395
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Read the signed-in account ID without a request and without credentials.
|
|
399
|
+
*
|
|
400
|
+
* GateMux registers no device, so `deviceId` is omitted rather than invented.
|
|
401
|
+
* The ID comes from the last successful profile read for this same grant; a
|
|
402
|
+
* caller that reads it before that profile read gets the OS version alone.
|
|
403
|
+
*
|
|
404
|
+
* @returns the Node platform version, plus the account ID while a profile for the stored grant is cached.
|
|
405
|
+
*/
|
|
406
|
+
async getDeviceIdentity() {
|
|
407
|
+
const lifetime = this.detailsLifetime
|
|
408
|
+
const grant = await this.readGrant(lifetime)
|
|
409
|
+
const profile = grant !== null && this.lastProfile?.token === grant.accessToken
|
|
410
|
+
? this.lastProfile.profile
|
|
411
|
+
: null
|
|
412
|
+
return {
|
|
413
|
+
...(profile?.id == null ? {} : { userId: profile.id }),
|
|
414
|
+
osVersion: `${platform()} ${release()}`,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
396
418
|
/**
|
|
397
419
|
* Run one authorization attempt: PKCE, browser callback, code exchange, commit.
|
|
398
420
|
* @param session - the attempt's cancellation signal and commit channel.
|
package/llm.js
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* already claims that route with the application's server key, and claiming it
|
|
10
10
|
* twice would throw `DUPLICATE_ADAPTER` and break plugin loading.
|
|
11
11
|
*
|
|
12
|
-
* Endpoint,
|
|
13
|
-
* `cordis.patch.yml`, so the composition remains the single source
|
|
14
|
-
* deployment choices.
|
|
12
|
+
* Endpoint, retry policy, and the fallback catalog come from this entry's
|
|
13
|
+
* `config` in `cordis.patch.yml`, so the composition remains the single source
|
|
14
|
+
* of truth for deployment choices. Which models the route serves is read from
|
|
15
|
+
* the endpoint's `/v1/models` listing instead; see `mergeCatalog` for how the
|
|
16
|
+
* two combine.
|
|
15
17
|
*
|
|
16
18
|
* @module @gatemux/dsh-gatemux-login/llm
|
|
17
19
|
*/
|
|
@@ -27,6 +29,107 @@ export const PROVIDER = 'gatemux-account'
|
|
|
27
29
|
export const name = 'gatemux-account'
|
|
28
30
|
export const inject = ['llm']
|
|
29
31
|
|
|
32
|
+
/** Listing value a model must carry to be reachable through this route's Messages transport. */
|
|
33
|
+
const MESSAGES_PROTOCOL = 'anthropic-messages'
|
|
34
|
+
|
|
35
|
+
/** Protocol version this transport sends on every request, the model listing included. */
|
|
36
|
+
const ANTHROPIC_VERSION = '2023-06-01'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Anthropic's listing path, read from a Messages base URL.
|
|
40
|
+
*
|
|
41
|
+
* The listing lives at `{root}/v1/models`, where the root is the base without a
|
|
42
|
+
* trailing slash and without one trailing `/v1` segment; GateMux publishes both
|
|
43
|
+
* spellings of the same root. `messagesApiRoot` implements this normalization
|
|
44
|
+
* for the transport but is not exported from the package, so the rule is
|
|
45
|
+
* restated here rather than pulled in through a deep import.
|
|
46
|
+
*
|
|
47
|
+
* @param baseURL - the route's configured Messages base URL.
|
|
48
|
+
* @returns the absolute model listing URL.
|
|
49
|
+
*/
|
|
50
|
+
function listingUrl(baseURL) {
|
|
51
|
+
const base = baseURL.replace(/\/+$/u, '')
|
|
52
|
+
return `${base.endsWith('/v1') ? base.slice(0, -3) : base}/v1/models`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read one positive catalog number, treating a missing, null, or unusable value as absent.
|
|
57
|
+
* @param value - candidate field from a listing entry.
|
|
58
|
+
* @returns the number, or `undefined` when the endpoint did not state a usable one.
|
|
59
|
+
*/
|
|
60
|
+
function positiveInteger(value) {
|
|
61
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read one non-empty catalog string, treating anything else as absent.
|
|
66
|
+
* @param value - candidate field from a listing entry.
|
|
67
|
+
* @returns the string, or `undefined` when the endpoint did not state a usable one.
|
|
68
|
+
*/
|
|
69
|
+
function nonEmptyString(value) {
|
|
70
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Project one listing entry into a catalog entry, or drop it as unserved.
|
|
75
|
+
*
|
|
76
|
+
* `protocols` is the criterion, not `category`: a chat model is not
|
|
77
|
+
* necessarily reachable through `/v1/messages`. GateMux reports `kimi-k3` as a
|
|
78
|
+
* chat model that speaks only `openai-completions`, and advertising it here
|
|
79
|
+
* would offer the user a model every request to this route fails on.
|
|
80
|
+
*
|
|
81
|
+
* @param entry - one element of the listing's `data` array.
|
|
82
|
+
* @returns the catalog entry, or `undefined` when this route cannot serve it.
|
|
83
|
+
*/
|
|
84
|
+
function servedModel(entry) {
|
|
85
|
+
if (entry === null || typeof entry !== 'object') return undefined
|
|
86
|
+
const id = nonEmptyString(entry.id)
|
|
87
|
+
const protocols = Array.isArray(entry.protocols) ? entry.protocols : []
|
|
88
|
+
if (id === undefined || !protocols.includes(MESSAGES_PROTOCOL)) return undefined
|
|
89
|
+
const name = nonEmptyString(entry.name)
|
|
90
|
+
const contextWindow = positiveInteger(entry.contextWindow)
|
|
91
|
+
const maxTokens = positiveInteger(entry.maxTokens)
|
|
92
|
+
// `capabilities` describes the model; `inputModalities` describes what this
|
|
93
|
+
// transport may send. Only vision is projected, because a model advertising
|
|
94
|
+
// image input that the endpoint rejects would fail on every later turn.
|
|
95
|
+
const vision = Array.isArray(entry.capabilities) && entry.capabilities.includes('vision')
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
...name === undefined ? {} : { name },
|
|
99
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
100
|
+
...maxTokens === undefined ? {} : { maxTokens },
|
|
101
|
+
inputModalities: vision ? ['text', 'image'] : ['text'],
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Combine the endpoint's served models with the declared catalog.
|
|
107
|
+
*
|
|
108
|
+
* A declared entry keeps every field it states, so the deployment's reviewed
|
|
109
|
+
* values win over fetched ones and a profile can still pin a model the endpoint
|
|
110
|
+
* reports differently; the endpoint fills only what the declaration leaves out.
|
|
111
|
+
* Wire-behavior fields — `systemPromptUpdate`, `toolUpdate`, the image budgets —
|
|
112
|
+
* exist only in a declaration, because they describe how to talk to the
|
|
113
|
+
* endpoint rather than a property of the model.
|
|
114
|
+
*
|
|
115
|
+
* Declared models the endpoint does not list are retained: an endpoint that
|
|
116
|
+
* omits an entry, or drops one, must not empty a route the operator configured.
|
|
117
|
+
*
|
|
118
|
+
* @param declared - catalog from this entry's `config`.
|
|
119
|
+
* @param remote - models the endpoint reported as served.
|
|
120
|
+
* @returns the effective catalog: remote entries in listing order, then declared
|
|
121
|
+
* entries the endpoint did not list in their own order.
|
|
122
|
+
*/
|
|
123
|
+
function mergeCatalog(declared, remote) {
|
|
124
|
+
const declaredById = new Map(declared.map(model => [model.id, model]))
|
|
125
|
+
const merged = remote.map((model) => {
|
|
126
|
+
const declaration = declaredById.get(model.id)
|
|
127
|
+
return declaration === undefined ? model : { ...model, ...declaration }
|
|
128
|
+
})
|
|
129
|
+
const listed = new Set(remote.map(model => model.id))
|
|
130
|
+
return [...merged, ...declared.filter(model => !listed.has(model.id))]
|
|
131
|
+
}
|
|
132
|
+
|
|
30
133
|
/**
|
|
31
134
|
* Register the GateMux account-authenticated provider route.
|
|
32
135
|
* @param ctx - Host context with the LLM registry injected.
|
|
@@ -34,9 +137,40 @@ export const inject = ['llm']
|
|
|
34
137
|
*/
|
|
35
138
|
export function apply(ctx, config) {
|
|
36
139
|
const logger = ctx.logger('gatemux-account')
|
|
37
|
-
const
|
|
140
|
+
const baseOptions = () => resolveAdapterOptions(plainOptions(config), launchEnvironmentOf(ctx))
|
|
38
141
|
// Validate the composed configuration now so a misconfiguration fails loudly at load.
|
|
39
|
-
|
|
142
|
+
baseOptions()
|
|
143
|
+
|
|
144
|
+
// The catalog the endpoint last reported. Both the advertised model list and
|
|
145
|
+
// every later capability resolution read it, because `modelInfo` derives a
|
|
146
|
+
// model's context window and input modalities from `connection.models`: a
|
|
147
|
+
// model that only reached the listing would be resolved as text-only with the
|
|
148
|
+
// connection's fallback context window. Null until one read succeeds, which
|
|
149
|
+
// leaves the declared catalog in force before the first sign-in.
|
|
150
|
+
let discovered = null
|
|
151
|
+
const options = () => {
|
|
152
|
+
const base = baseOptions()
|
|
153
|
+
return discovered === null ? base : { ...base, models: mergeCatalog(base.models, discovered) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read the served models from the endpoint's listing.
|
|
158
|
+
* @param connection - resolved route configuration carrying the endpoint base.
|
|
159
|
+
* @param headers - request authentication for the signed-in user.
|
|
160
|
+
* @returns the catalog entries this route can serve, in listing order.
|
|
161
|
+
*/
|
|
162
|
+
const loadCatalog = async (connection, headers) => {
|
|
163
|
+
const url = listingUrl(connection.baseURL)
|
|
164
|
+
const response = await fetch(url, {
|
|
165
|
+
method: 'GET',
|
|
166
|
+
headers: { ...headers, 'accept': 'application/json', 'anthropic-version': ANTHROPIC_VERSION },
|
|
167
|
+
})
|
|
168
|
+
if (!response.ok) throw new Error(`${url} answered ${response.status}`)
|
|
169
|
+
const body = await response.json()
|
|
170
|
+
const entries = body !== null && typeof body === 'object' && Array.isArray(body.data) ? body.data : undefined
|
|
171
|
+
if (entries === undefined) throw new Error(`${url} did not answer with a model list`)
|
|
172
|
+
return entries.map(servedModel).filter(model => model !== undefined)
|
|
173
|
+
}
|
|
40
174
|
|
|
41
175
|
const resolveAuth = async (connection) => {
|
|
42
176
|
const account = ctx.get('deepseekAccount')
|
|
@@ -82,15 +216,31 @@ export function apply(ctx, config) {
|
|
|
82
216
|
resolveAuth,
|
|
83
217
|
providerName: 'GateMux Account',
|
|
84
218
|
discoverModels: async (provider) => {
|
|
85
|
-
const connection =
|
|
219
|
+
const connection = baseOptions()
|
|
220
|
+
let auth
|
|
86
221
|
try {
|
|
87
|
-
await resolveAuth(connection)
|
|
222
|
+
auth = await resolveAuth(connection)
|
|
88
223
|
} catch (error) {
|
|
89
224
|
// With no signed-in user the route advertises nothing rather than failing discovery.
|
|
90
225
|
if (error instanceof LlmError && error.code === 'ACCOUNT_SIGN_IN_REQUIRED') return []
|
|
91
226
|
throw error
|
|
92
227
|
}
|
|
93
|
-
|
|
228
|
+
try {
|
|
229
|
+
discovered = await loadCatalog(connection, auth.headers)
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// The listing is an auxiliary surface. A failed read keeps the catalog
|
|
232
|
+
// that is already in force — the declared one before the first
|
|
233
|
+
// success, the last reported one after — rather than emptying the
|
|
234
|
+
// route's model list and dropping the provider from the selector. A
|
|
235
|
+
// credential this endpoint rejects is still classified where it is
|
|
236
|
+
// acted on, by `onRequestError` on the model request itself.
|
|
237
|
+
logger.warn(
|
|
238
|
+
`GateMux model listing read failed at ${listingUrl(connection.baseURL)}; `
|
|
239
|
+
+ `keeping the ${discovered === null ? 'declared' : 'last reported'} catalog`,
|
|
240
|
+
error,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
return options().models.map((model) => catalogModelInfo(provider, model))
|
|
94
244
|
},
|
|
95
245
|
})
|
|
96
246
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatemux/dsh-gatemux-login",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sign in with GateMux for DeepSeek Harness: users pay from their own GateMux balance.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/discovery.spec.mjs"
|
|
8
|
+
},
|
|
6
9
|
"publishConfig": {
|
|
7
10
|
"access": "public"
|
|
8
11
|
},
|
|
@@ -29,22 +32,22 @@
|
|
|
29
32
|
},
|
|
30
33
|
"peerDependencies": {
|
|
31
34
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
32
|
-
"@deepseek-ai/dsh-authorization": "^0.1.7-rc.2",
|
|
33
|
-
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
|
|
34
|
-
"@deepseek-ai/dsh-deepseek-account": "^0.1.7-rc.2",
|
|
35
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
|
|
36
|
-
"@deepseek-ai/dsh-launch-environment": "^0.1.7-rc.2",
|
|
37
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
|
|
38
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.1.7-rc.2"
|
|
35
|
+
"@deepseek-ai/dsh-authorization": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
36
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
37
|
+
"@deepseek-ai/dsh-deepseek-account": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
38
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
39
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.7-rc.2 || ^0.2.0-rc.2"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
42
|
-
"@deepseek-ai/dsh-authorization": "^0.1.7-rc.2",
|
|
43
|
-
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
|
|
44
|
-
"@deepseek-ai/dsh-deepseek-account": "^0.1.7-rc.2",
|
|
45
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
|
|
46
|
-
"@deepseek-ai/dsh-launch-environment": "^0.1.7-rc.2",
|
|
47
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
|
|
48
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.1.7-rc.2"
|
|
45
|
+
"@deepseek-ai/dsh-authorization": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-deepseek-account": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.7-rc.2 || ^0.2.0-rc.2"
|
|
49
52
|
}
|
|
50
53
|
}
|