@100xprompt/brain 0.1.1 → 0.1.3

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 CHANGED
@@ -96,6 +96,9 @@ brain gateway logout
96
96
  - Connecting changes which model the tool uses: it runs on 100xprompt Pro by default (`--model flash` for 100xprompt
97
97
  Flash), billed to your 100xprompt account. Context editing is on by default; `--no-context-edit` turns it off and is
98
98
  remembered for later connects until `--context-edit`.
99
+ - `--model auto` connects the tool to 100xprompt Auto (the gateway's `/auto` route): each kind of work (everyday asks,
100
+ code, reasoning) moves to a cheaper model only when your company's own test of its logs (in the Savings app) proved it
101
+ no worse, and stays on 100xprompt Pro until then. `--model pro` or `--model flash` switches back.
99
102
  - Before changing anything, `connect` checks that the gateway accepts your key the way that tool sends it (a free check
100
103
  that generates nothing). If your account's quota is used up, it still connects and tells you, in the gateway's words.
101
104
  - Only the settings brain owns are written; the value each had before is recorded and put back by `disconnect`. The key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@100xprompt/brain",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One working session, any AI coding tool: move sessions between Claude Code, Codex, Cursor, Gemini CLI, OpenCode and more, keep what projects learned, spend fewer tokens. Runs on Bun. By 100xprompt.",
5
5
  "license": "MIT",
6
6
  "author": "Nipurn Agarwal",
@@ -8,7 +8,7 @@ import { join } from "node:path"
8
8
  import type { Reader } from "../adapters/types.ts"
9
9
  import { toolLabel } from "../brief.ts"
10
10
  import { DEFAULT_MODEL, DEFAULT_ROUTE, type Env, type GatewaySettings, modelName, statePath } from "../gateway/common.ts"
11
- import { CONNECTORS, hasKey, installedHere } from "../gateway/connect.ts"
11
+ import { CONNECTORS, cliAuthPath, hasKey, installedHere } from "../gateway/connect.ts"
12
12
  import { HOOK_TOOLS, hookInstalled } from "../hooks.ts"
13
13
  import { MCP_TOOLS, statusMcp } from "../memory/mcp-install.ts"
14
14
  import { chittaCandidates, memoryDbPath, openMemory } from "../memory/store.ts"
@@ -182,6 +182,20 @@ export async function harnesses(home: string, counts: Record<string, ToolCount>
182
182
  return false
183
183
  }
184
184
  }
185
+ /**
186
+ * The 100xprompt CLI is the gateway's own client: there is nothing for brain to connect, and it is on the gateway
187
+ * whenever it is signed in (its auth.json holds a key; only its presence is looked at). Signed out, it can be
188
+ * (`100xprompt auth login`). Any other tool: undefined, decided by brain's connectors.
189
+ */
190
+ const ownClient = (id: string, h: string, e: Env): Harness["gateway"] | undefined => {
191
+ if (id !== "100xprompt") return undefined
192
+ try {
193
+ const k = JSON.parse(readFileSync(cliAuthPath(h, e), "utf8"))?.["100xprompt"]?.key
194
+ return typeof k === "string" && k.trim() ? "connected" : "available"
195
+ } catch {
196
+ return "available"
197
+ }
198
+ }
185
199
  const known = new Map<string, { label: string; present: boolean }>()
186
200
  const add = (id: string, label: string, present: boolean) => {
187
201
  const k = known.get(id)
@@ -207,7 +221,7 @@ export async function harnesses(home: string, counts: Record<string, ToolCount>
207
221
  label: clip(k.label, 80),
208
222
  sessions: counts?.[id] ? counts[id]!.sessions : null,
209
223
  lastSessionAt: counts?.[id]?.lastSessionAt ?? null,
210
- gateway: connected[id] ? "connected" : connectorIds.has(id) ? "available" : "unsupported",
224
+ gateway: connected[id] ? "connected" : (ownClient(id, home, env) ?? (connectorIds.has(id) ? "available" : "unsupported")),
211
225
  mcp: mcpIds.has(id) ? mcpInstalled.get(id) === true : null,
212
226
  hooks: HOOK_TOOLS.includes(id) ? hookOn(id) : null,
213
227
  }))
@@ -12,15 +12,66 @@ export const DEFAULT_ROUTE = "flash"
12
12
  export const DEFAULT_MODEL = "gemini-3.8-flash"
13
13
  export const EDIT_HEADER = "x-100x-context-edit"
14
14
 
15
+ /**
16
+ * `--model auto`: the gateway's auto-routing (the router's docs/routing-contract.md, "The /auto route"). Its route serves
17
+ * only this model id, and this id only on its route: per kind of work (everyday asks, code, reasoning) the gateway serves
18
+ * a cheaper model where the company's own test in the Savings app proved it no worse, and 100xprompt Pro everywhere else.
19
+ */
20
+ export const AUTO_ROUTE = "auto"
21
+ export const AUTO_MODEL = "100xprompt-auto"
22
+
15
23
  /**
16
24
  * What 100xprompt calls the models brain may connect a tool to, as the 100xprompt CLI shows them: tools are given the
17
25
  * gateway's model id (that is what the gateway serves), and everything brain prints uses the product name.
18
26
  */
19
- export const MODEL_NAMES: Record<string, string> = { "gemini-3.8-flash": "100xprompt Pro", "gemini-3.7-flash": "100xprompt Flash", "gemini-3.6-flash": "100xprompt Flash" }
20
- /** `--model pro` / `--model flash`: the product names, turned into the gateway's model ids. */
21
- export const MODEL_ALIASES: Record<string, string> = { pro: "gemini-3.8-flash", flash: "gemini-3.7-flash" }
27
+ export const MODEL_NAMES: Record<string, string> = {
28
+ "gemini-3.8-flash": "100xprompt Pro",
29
+ "gemini-3.7-flash": "100xprompt Flash",
30
+ "gemini-3.6-flash": "100xprompt Flash",
31
+ "gemini-3.5-flash": "100xprompt Flash",
32
+ [AUTO_MODEL]: "100xprompt Auto",
33
+ }
34
+ /** `--model pro` / `--model flash` / `--model auto`: the product names, turned into the gateway's model ids. */
35
+ export const MODEL_ALIASES: Record<string, string> = { pro: "gemini-3.8-flash", flash: "gemini-3.7-flash", auto: AUTO_MODEL }
22
36
  export const modelName = (id: string) => MODEL_NAMES[id] ?? id
23
37
  export const modelId = (given: string) => MODEL_ALIASES[given.trim().toLowerCase()] ?? given.trim()
38
+ /**
39
+ * Whether a connect uses auto-routing. No tool knows this id: a connector whose tool sizes its context window (or picks
40
+ * its capabilities) from the model id it is given tells it, for auto, what every model auto may serve takes (limitsFor).
41
+ */
42
+ export const isAuto = (s: Pick<GatewaySettings, "model">) => s.model === AUTO_MODEL
43
+ /** What the Gemini Flash models behind 100xprompt Pro and Flash take: 1,048,576 tokens in, 65,536 out. */
44
+ export const GEMINI_LIMITS = { context: 1_048_576, output: 65_536 } as const
45
+ /**
46
+ * What every model auto-routing may serve takes (the contract's table: the Gemini Flash models and DeepSeek V4 Flash): the
47
+ * smaller window, DeepSeek V4 Flash's 1,000,000 tokens, and Gemini's 65,536 out (DeepSeek allows more). Both take text and
48
+ * images; audio, video and PDF only Gemini, so a tool told what it may send gets images only.
49
+ */
50
+ export const AUTO_LIMITS = { context: 1_000_000, output: 65_536 } as const
51
+ /** The context window and output limit a connector tells its tool, for the model it connects. */
52
+ export const limitsFor = (s: Pick<GatewaySettings, "model">) => (isAuto(s) ? AUTO_LIMITS : GEMINI_LIMITS)
53
+
54
+ /**
55
+ * The route and model a connect uses, from the flags given (undefined: not given) and the settings stored by the last
56
+ * connect. The auto model and the auto route go together: `--model auto` picks the auto route (a different --route with
57
+ * it is refused), `--route auto` picks the auto model, and another model or route after an auto connect goes back to the
58
+ * default route (flash) or model (100xprompt Pro).
59
+ */
60
+ export function routeAndModel(given: { route?: string; model?: string }, stored: { route?: string; model?: string } = {}): { route: string; model: string } | { why: string } {
61
+ const model = given.model !== undefined ? modelId(given.model) : undefined
62
+ const route = given.route
63
+ if (model === AUTO_MODEL) {
64
+ if (route !== undefined && route !== AUTO_ROUTE) return { why: `--model auto uses the gateway's ${AUTO_ROUTE} route, so it cannot be combined with --route ${route}; leave out --route` }
65
+ return { route: AUTO_ROUTE, model }
66
+ }
67
+ if (route === AUTO_ROUTE) {
68
+ if (model !== undefined) return { why: `the ${AUTO_ROUTE} route chooses the model itself, so it cannot be combined with --model ${given.model!.trim()}; use --model auto` }
69
+ return { route: AUTO_ROUTE, model: AUTO_MODEL }
70
+ }
71
+ const wasAuto = stored.model === AUTO_MODEL || stored.route === AUTO_ROUTE
72
+ if (route === undefined && model === undefined) return wasAuto ? { route: AUTO_ROUTE, model: AUTO_MODEL } : { route: stored.route ?? DEFAULT_ROUTE, model: stored.model ?? DEFAULT_MODEL }
73
+ return { route: route ?? (wasAuto ? DEFAULT_ROUTE : (stored.route ?? DEFAULT_ROUTE)), model: model ?? (wasAuto ? DEFAULT_MODEL : (stored.model ?? DEFAULT_MODEL)) }
74
+ }
24
75
 
25
76
  export interface GatewaySettings {
26
77
  url: string
@@ -46,7 +97,8 @@ export function readKey(home: string): string {
46
97
  }
47
98
 
48
99
  /**
49
- * The gateway route's base address, e.g. https://proxy.100xprompt.com/flash. Under it the gateway serves, for every model:
100
+ * The gateway route's base address, e.g. https://proxy.100xprompt.com/flash (or /auto, which takes the same paths). Under
101
+ * it the gateway serves, for every model:
50
102
  * /v1/chat/completions OpenAI Chat Completions (Authorization: Bearer <key>)
51
103
  * /v1/responses OpenAI Responses (Authorization: Bearer <key>)
52
104
  * /v1/messages Anthropic Messages (x-api-key: <key>, or Authorization: Bearer)
@@ -34,9 +34,9 @@ import {
34
34
  type Json,
35
35
  keyPath,
36
36
  MODEL_NAMES,
37
- modelId,
38
37
  modelName,
39
38
  readKey,
39
+ routeAndModel,
40
40
  routeUrl,
41
41
  setAt,
42
42
  shellQuote,
@@ -46,6 +46,8 @@ import { EXTRA_CONNECTORS } from "./connectors/index.ts"
46
46
  import { askHidden, browserSignIn, type SignInDeps } from "./signin.ts"
47
47
 
48
48
  export {
49
+ AUTO_MODEL,
50
+ AUTO_ROUTE,
49
51
  DEFAULT_MODEL,
50
52
  DEFAULT_ROUTE,
51
53
  DEFAULT_URL,
@@ -593,15 +595,18 @@ export async function connect(
593
595
  if (!c) return { ok: false, lines: [`unknown tool "${toolId}"; \`brain gateway tools\` lists the ones that can be connected`] }
594
596
  if (!hasKey(home) && !o.dryRun) return { ok: false, lines: ["not signed in to the gateway yet: run `brain gateway login`"] }
595
597
  const st = loadState(home)
598
+ // --model auto goes with the auto route (and back to flash after it): common.ts routeAndModel.
599
+ const rm = routeAndModel({ route: o.settings?.route, model: o.settings?.model }, st.settings ?? {})
600
+ if ("why" in rm) return { ok: false, lines: [rm.why] }
596
601
  const s: GatewaySettings = {
597
602
  url: o.settings?.url ?? st.settings?.url ?? DEFAULT_URL,
598
- route: o.settings?.route ?? st.settings?.route ?? DEFAULT_ROUTE,
599
- model: o.settings?.model !== undefined ? modelId(o.settings.model) : (st.settings?.model ?? DEFAULT_MODEL),
603
+ route: rm.route,
604
+ model: rm.model,
600
605
  contextEdit: o.settings?.contextEdit ?? st.settings?.contextEdit ?? true,
601
606
  }
602
607
  if (!/^https?:\/\/[^\s]+$/.test(s.url) || (/^http:/.test(s.url) && !/^http:\/\/(127\.0\.0\.1|localhost)(:|\/|$)/.test(s.url))) return { ok: false, lines: ["--url must be https:// (http:// only for a gateway on this machine)"] }
603
608
  if (!/^[a-z0-9-]{1,40}$/.test(s.route)) return { ok: false, lines: ["--route must be a gateway route name such as flash or pro"] }
604
- if (!/^[\w.:/-]{1,80}$/.test(s.model)) return { ok: false, lines: ["--model must be pro, flash or a plain model name"] }
609
+ if (!/^[\w.:/-]{1,80}$/.test(s.model)) return { ok: false, lines: ["--model must be pro, flash, auto or a plain model name"] }
605
610
  const main = c.file(home, env)
606
611
  if (!installedHere(c, home, env)) return { ok: false, lines: [`${c.label} has not been run on this machine (nothing at ${main}); run it once, then connect it`] }
607
612
  let limit: string | undefined
@@ -748,7 +753,7 @@ const USAGE = [
748
753
  "brain gateway login [--no-browser] sign in to 100xprompt in your browser, as `100xprompt auth login` does",
749
754
  "brain gateway login --key-env VAR | --from-cli take the key from VAR (scripts, CI) or from the 100xprompt CLI's sign-in",
750
755
  "brain gateway logout forget the key",
751
- "brain gateway connect --tool <id> [--model pro|flash] [--route flash] [--url https://...] [--no-context-edit | --context-edit] [--dry-run] [--no-probe]",
756
+ "brain gateway connect --tool <id> [--model pro|flash|auto] [--route flash] [--url https://...] [--no-context-edit | --context-edit] [--dry-run] [--no-probe]",
752
757
  "brain gateway disconnect --tool <id> [--dry-run]",
753
758
  "brain gateway status | tools",
754
759
  ]
@@ -29,15 +29,12 @@
29
29
  // Docs: https://aider.chat/docs/config/adv-model-settings.html (extra_params, extra_headers, metadata file),
30
30
  // https://aider.chat/docs/llms/openai-compat.html (openai/<model> against an OpenAI-compatible API).
31
31
  import { join } from "node:path"
32
- import { type Codec, type Connector, EDIT_HEADER, type Json, readKey, routeUrl, yamlTopLevel } from "../common.ts"
32
+ import { type Codec, type Connector, EDIT_HEADER, type Json, limitsFor, readKey, routeUrl, yamlTopLevel } from "../common.ts"
33
33
 
34
34
  /** The one key of brain's own entry in the settings list and in the metadata object. */
35
35
  export const AIDER_ENTRY = "brain-gateway"
36
36
  /** Marks brain's entry in each file (a YAML comment, a JSON5 comment), so it is told apart from the person's own. */
37
37
  export const AIDER_MARK = "100xprompt gateway, added by brain gateway connect --tool aider"
38
- /** What Aider is told about the gateway's model: the Gemini Flash models' window and output limit. */
39
- const CONTEXT = 1_048_576
40
- const OUTPUT = 65_536
41
38
 
42
39
  const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
43
40
 
@@ -337,7 +334,8 @@ export const connectors: Connector[] = [
337
334
  path: join(home, ".aider.model.metadata.json"),
338
335
  format: "custom",
339
336
  codec: modelMetadataJson,
340
- values: { [AIDER_ENTRY]: { [model]: { max_input_tokens: CONTEXT, max_output_tokens: OUTPUT, max_tokens: OUTPUT, litellm_provider: "openai", mode: "chat" } } },
337
+ // The Gemini Flash models' window and output limit, or for 100xprompt Auto what every model it may serve takes.
338
+ values: { [AIDER_ENTRY]: { [model]: { max_input_tokens: limitsFor(s).context, max_output_tokens: limitsFor(s).output, max_tokens: limitsFor(s).output, litellm_provider: "openai", mode: "chat" } } },
341
339
  },
342
340
  {
343
341
  path: join(home, ".aider.conf.yml"),
@@ -26,12 +26,9 @@
26
26
  // not followed; CLINE_DIR and CLINE_DATA_DIR are, when they exist. The VS Code extension (id `cline`, see cline.ts) shares
27
27
  // ~/.cline/data but chooses its provider from its own globalState.json, so this connector does not change it.
28
28
  import { join } from "node:path"
29
- import { type Connector, dirFromEnv, EDIT_HEADER, type Env, modelName, readKey, routeUrl } from "../common.ts"
29
+ import { type Connector, dirFromEnv, EDIT_HEADER, type Env, limitsFor, modelName, readKey, routeUrl } from "../common.ts"
30
30
 
31
31
  const ID = "brain-gateway"
32
- /** Gemini 3.x Flash, which the gateway's models are: a 1M-token window, 64K out. */
33
- const CONTEXT_WINDOW = 1_048_576
34
- const MAX_OUTPUT = 65_536
35
32
  /** Cline requires an ISO date here; a fixed one keeps edits() free of side effects (Cline sets its own when it saves). */
36
33
  const UPDATED_AT = "2026-01-01T00:00:00.000Z"
37
34
 
@@ -56,7 +53,7 @@ export const connectors: Connector[] = [
56
53
  version: 1,
57
54
  [`providers/${ID}`]: {
58
55
  provider: { name: "100xprompt gateway", baseUrl, defaultModelId: s.model, protocol: "openai-chat", client: "openai-compatible", capabilities: ["tools", "streaming"] },
59
- models: { [s.model]: { id: s.model, name: modelName(s.model), contextWindow: CONTEXT_WINDOW, maxTokens: MAX_OUTPUT, capabilities: ["tools", "streaming", "images"] } },
56
+ models: { [s.model]: { id: s.model, name: modelName(s.model), contextWindow: limitsFor(s).context, maxTokens: limitsFor(s).output, capabilities: ["tools", "streaming", "images"] } },
60
57
  },
61
58
  },
62
59
  },
@@ -30,7 +30,8 @@
30
30
  // apiKey: ${{ secrets.BRAIN_GATEWAY_KEY }}, roles chat/edit/apply/summarize,
31
31
  // capabilities tool_use and image_input (Continue only guesses tool support for provider openai from
32
32
  // gpt-/o- model names, toolSupport.ts ~277661, so without it Agent mode would be off),
33
- // contextLength 1048576 (an unknown model gets 32768, ~238017), and the context-edit header.
33
+ // contextLength 1048576 (an unknown model gets 32768, ~238017; for 100xprompt Auto the
34
+ // smallest window auto may serve, common.ts limitsFor), and the context-edit header.
34
35
  // First in the list makes it the default: the extension and the CLI take the first chat model unless
35
36
  // one was picked before (extension rectifySelectedModelsFromGlobalContext ~798703; CLI
36
37
  // services/ModelService.ts:53-120 and util/modelPersistence.ts, `cliSelectedModel`).
@@ -42,7 +43,7 @@
42
43
  // stay, and removing the entry gives back the exact bytes, including an empty `models: []` or a list that was not there.
43
44
  import { existsSync, readFileSync } from "node:fs"
44
45
  import { isAbsolute, join, resolve } from "node:path"
45
- import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, modelName, readKey, routeUrl, yamlTopLevel } from "../common.ts"
46
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, limitsFor, modelName, readKey, routeUrl, yamlTopLevel } from "../common.ts"
46
47
 
47
48
  /** The name the key is kept under in the tool's .env, and that its config refers to. */
48
49
  export const SECRET = "BRAIN_GATEWAY_KEY"
@@ -50,8 +51,6 @@ export const SECRET = "BRAIN_GATEWAY_KEY"
50
51
  const OWN_CONFIG = "100xprompt gateway"
51
52
  /** The codec key of brain's entry in the models list (the entry itself is found by its apiKey, so a model switch keeps it). */
52
53
  export const ENTRY_KEY = "models/100xprompt gateway"
53
- /** Gemini models' context window; Continue assumes 32768 for a model it does not know. */
54
- const CONTEXT = 1048576
55
54
 
56
55
  const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
57
56
  const isPlainObject = (v: unknown): v is Json => !!v && typeof v === "object" && !Array.isArray(v)
@@ -521,7 +520,7 @@ const headers = (s: GatewaySettings) => (s.contextEdit ? { requestOptions: { hea
521
520
 
522
521
  /** The models entry in config.json form (older Continue, PearAI): `title`, the key by env name. */
523
522
  export function jsonEntry(s: GatewaySettings, extra: Json = {}): Json {
524
- return { title: modelName(s.model), provider: "openai", model: s.model, apiBase: `${routeUrl(s)}/v1`, apiKey: SECRET, contextLength: CONTEXT, ...extra, ...headers(s) }
523
+ return { title: modelName(s.model), provider: "openai", model: s.model, apiBase: `${routeUrl(s)}/v1`, apiKey: SECRET, contextLength: limitsFor(s).context, ...extra, ...headers(s) }
525
524
  }
526
525
 
527
526
  /** The .env edit both Continue and PearAI read their secret from. */
@@ -557,7 +556,7 @@ export const connectors: Connector[] = [
557
556
  apiKey: `\${{ secrets.${SECRET} }}`,
558
557
  roles: ["chat", "edit", "apply", "summarize"],
559
558
  capabilities: ["tool_use", "image_input"],
560
- defaultCompletionOptions: { contextLength: CONTEXT },
559
+ defaultCompletionOptions: { contextLength: limitsFor(s).context },
561
560
  ...headers(s),
562
561
  }
563
562
  return [
@@ -30,7 +30,7 @@
30
30
  // A model list is given, so Crush does not ask the gateway for /v1/models (403 there).
31
31
  import { existsSync } from "node:fs"
32
32
  import { isAbsolute, join, resolve } from "node:path"
33
- import { type Codec, type Connector, EDIT_HEADER, type Env, getAt, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
33
+ import { type Codec, type Connector, EDIT_HEADER, type Env, getAt, type Json, keyPath, limitsFor, modelName, routeUrl, shellQuote } from "../common.ts"
34
34
 
35
35
  /** Crush's data file, resolved as Crush does; the env overrides describe the person's real home, so they count only there. */
36
36
  export function crushDataFile(home: string, env: Env): string {
@@ -297,7 +297,7 @@ export const connectors: Connector[] = [
297
297
  base_url: `${routeUrl(s)}/v1`,
298
298
  api_key: `$(cat ${shellQuote(keyPath(home))})`,
299
299
  ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
300
- models: [{ id: s.model, name: modelName(s.model), context_window: 1048576, default_max_tokens: 65536, capabilities: { vision: true } }],
300
+ models: [{ id: s.model, name: modelName(s.model), context_window: limitsFor(s).context, default_max_tokens: limitsFor(s).output, capabilities: { vision: true } }],
301
301
  },
302
302
  "models/large": pick,
303
303
  "models/small": pick,
@@ -14,6 +14,10 @@
14
14
  // stdout."), run with shell:true, cached for 5 minutes (apiKeyHelperTtlMs, FACTORY_API_KEY_HELPER_TTL_MS). So the key
15
15
  // is read from the brain's key file at run time, never copied, and a new sign-in reaches Droid without a reconnect.
16
16
  // - `extraHeaders` is sent with every request (case-insensitively unique names), so the context-edit header can go.
17
+ // - Context window and output limit: Droid infers a custom model's from its model id (the built-in model whose
18
+ // matchPatterns match it: gemini-3.8-flash gets Gemini 3.8 Flash's 1M in and 64K out), and an entry's maxContextLimit
19
+ // and maxOutputTokens take precedence (k4e and ev in the binary). 100xprompt Auto's id matches nothing, so for auto they
20
+ // are given: what every model auto may serve takes (common.ts limitsFor). Images stay on (noImageSupport unset).
17
21
  // - A model's id is `custom:<displayName with spaces as ->-<n>` unless the entry names its own `id`; this connector gives
18
22
  // its entry the id `custom:brain-gateway`, so the default stays put whatever else is in the list.
19
23
  // - The default model is `sessionDefaultSettings.model` (older top-level `model` is folded into it on read and only
@@ -27,7 +31,7 @@
27
31
  // - Checked for real (scripts/gateway-real/factory-droid.ts): `droid exec` with no -m, no Factory login, sends its
28
32
  // request to <route>/v1/messages with x-api-key from the helper, the context-edit header and the gateway's model.
29
33
  import { join } from "node:path"
30
- import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
34
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, isAuto, type Json, keyPath, limitsFor, modelName, routeUrl, shellQuote } from "../common.ts"
31
35
 
32
36
  /** The id of the connector's entry in customModels, and the default model value that selects it. */
33
37
  export const DROID_MODEL_ID = "custom:brain-gateway"
@@ -295,6 +299,7 @@ export const connectors: Connector[] = [
295
299
  provider: "anthropic",
296
300
  apiKeyHelper: `cat ${shellQuote(keyPath(home))}`,
297
301
  ...(s.contextEdit ? { extraHeaders: { [EDIT_HEADER]: "on" } } : {}),
302
+ ...(isAuto(s) ? { maxContextLimit: limitsFor(s).context, maxOutputTokens: limitsFor(s).output } : {}),
298
303
  },
299
304
  "sessionDefaultSettings/model": DROID_MODEL_ID,
300
305
  },
@@ -22,12 +22,10 @@
22
22
  // The binary's strings list the same field names (`strings goose | grep api_key_env`), and the real-tool check
23
23
  // (scripts/gateway-real/goose.ts) runs this exact setup.
24
24
  import { isAbsolute, join, resolve } from "node:path"
25
- import { type Connector, EDIT_HEADER, type Env, keyPath, modelName, routeUrl, yamlTopLevel } from "../common.ts"
25
+ import { type Connector, EDIT_HEADER, type Env, keyPath, limitsFor, modelName, routeUrl, yamlTopLevel } from "../common.ts"
26
26
 
27
27
  /** The provider id (the custom provider's file name and `name`, and GOOSE_PROVIDER), as Codex and OpenCode name it. */
28
28
  export const GOOSE_PROVIDER = "brain-gateway"
29
- /** The context window Goose is told (it compacts the conversation near it): the Gemini Flash models' 1,048,576 tokens. */
30
- const CONTEXT_LIMIT = 1_048_576
31
29
 
32
30
  /** Goose's config folder, found the way Goose finds it. XDG_CONFIG_HOME describes the real home, so it counts only there. */
33
31
  export function gooseDir(home: string, env: Env): string {
@@ -56,7 +54,9 @@ export const connectors: Connector[] = [
56
54
  display_name: "100xprompt gateway",
57
55
  description: `${modelName(s.model)} through the 100xprompt gateway (set up by brain gateway connect --tool goose)`,
58
56
  base_url: `${routeUrl(s)}/v1`,
59
- models: [{ name: s.model, context_limit: CONTEXT_LIMIT }],
57
+ // The window Goose is told (it compacts the conversation near it): the Gemini Flash models' 1,048,576 tokens, or
58
+ // for 100xprompt Auto the smallest window it may serve (common.ts limitsFor).
59
+ models: [{ name: s.model, context_limit: limitsFor(s).context }],
60
60
  ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
61
61
  supports_streaming: true,
62
62
  requires_auth: true,
@@ -19,7 +19,10 @@
19
19
  // - `extra_headers` on the provider entry is sent with every request on the OpenAI wire (runtime_provider.py
20
20
  // _lift_extra_headers), so the context-edit header can go.
21
21
  // - The gateway's /v1/models answers 403; Hermes then takes the context window from its catalog ("gemini" → 1M,
22
- // agent/model_metadata.py), so nothing else needs setting.
22
+ // agent/model_metadata.py), so nothing else needs setting for a Gemini id. 100xprompt Auto's id is in no catalog (Hermes
23
+ // would assume 256,000: DEFAULT_FALLBACK_CONTEXT), so for auto the entry also gets `models: {<id>: {context_length}}`,
24
+ // the per-model override Hermes checks before any catalog (hermes_cli/config.py get_custom_provider_context_length,
25
+ // matched by the entry's base_url): what every model auto may serve takes (common.ts limitsFor).
23
26
  // - $HERMES_HOME replaces ~/.hermes when set (a Hermes profile is its own HERMES_HOME).
24
27
  // - Checked for real (scripts/gateway-real/hermes.ts): `hermes chat -q` sends its request to <route>/v1/chat/completions
25
28
  // with Authorization: Bearer <key from .env>, the context-edit header and the gateway's model, even with the person's
@@ -27,7 +30,7 @@
27
30
  // config.yaml is YAML with comments; the codec below edits `<section>/<entry>` lines in place and leaves every other line
28
31
  // as it was.
29
32
  import { join } from "node:path"
30
- import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type Json, readKey, routeUrl, yamlTopLevel } from "../common.ts"
33
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, isAuto, type Json, limitsFor, readKey, routeUrl, yamlTopLevel } from "../common.ts"
31
34
 
32
35
  export const HERMES_PROVIDER = "brain-gateway"
33
36
  export const HERMES_KEY_VAR = "BRAIN_GATEWAY_API_KEY"
@@ -209,6 +212,7 @@ export const connectors: Connector[] = [
209
212
  api_mode: "chat_completions",
210
213
  default_model: s.model,
211
214
  ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
215
+ ...(isAuto(s) ? { models: { [s.model]: { context_length: limitsFor(s).context } } } : {}),
212
216
  },
213
217
  "model/provider": `custom:${HERMES_PROVIDER}`,
214
218
  "model/default": s.model,
@@ -26,7 +26,7 @@
26
26
  // back the exact bytes. Before any change the codec makes a trial edit and refuses a file where that would not land
27
27
  // exactly on brain's entries (an inline `providers = {...}`, dotted keys, sub-tables of brain's tables, ...).
28
28
  import { join } from "node:path"
29
- import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Json, modelName, readKey, routeUrl } from "../common.ts"
29
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Json, limitsFor, modelName, readKey, routeUrl } from "../common.ts"
30
30
 
31
31
  export const kimiConfig = (home: string, env: Record<string, string | undefined>) => join(dirFromEnv(env, "KIMI_CODE_HOME", join(home, ".kimi-code")), "config.toml")
32
32
 
@@ -227,7 +227,7 @@ export const connectors: Connector[] = [
227
227
  [`models/${PROVIDER}`]: {
228
228
  provider: PROVIDER,
229
229
  model: s.model,
230
- max_context_size: 1048576,
230
+ max_context_size: limitsFor(s).context,
231
231
  capabilities: ["image_in", "tool_use"],
232
232
  display_name: modelName(s.model),
233
233
  },
@@ -33,7 +33,7 @@
33
33
  // (src/backend/dev/pi-request-headers.ts).
34
34
  import { existsSync } from "node:fs"
35
35
  import { isAbsolute, join } from "node:path"
36
- import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, keyPath, modelName, routeUrl } from "../common.ts"
36
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, keyPath, limitsFor, modelName, routeUrl } from "../common.ts"
37
37
 
38
38
  export const MOD_FILE = "brain-gateway.mjs"
39
39
  const MARK = "// Written by `brain gateway connect --tool letta-code`"
@@ -136,18 +136,15 @@ export const lettaMod: Codec = {
136
136
  },
137
137
  }
138
138
 
139
- /** What 100xprompt's models take (Gemini 3.x Flash: 1M tokens in, 64k out). */
140
- const CONTEXT_WINDOW = 1_048_576
141
- const MAX_TOKENS = 65_536
142
-
143
139
  const modValues = (home: string, s: GatewaySettings): Record<string, unknown> => ({
144
140
  provider: "brain-gateway",
145
141
  name: "100xprompt gateway",
146
142
  baseUrl: `${routeUrl(s)}/v1`,
147
143
  model: s.model,
148
144
  modelName: modelName(s.model),
149
- contextWindow: CONTEXT_WINDOW,
150
- maxTokens: MAX_TOKENS,
145
+ // What the model takes (Gemini 3.x Flash: 1M tokens in, 64k out; for 100xprompt Auto, what every model it may serve takes).
146
+ contextWindow: limitsFor(s).context,
147
+ maxTokens: limitsFor(s).output,
151
148
  keyFile: keyPath(home),
152
149
  ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
153
150
  })
@@ -25,9 +25,14 @@
25
25
  // (routing metadata for the previous endpoint; chat_options.py:83-85 sends it as extra_body). All of them come back
26
26
  // on disconnect.
27
27
  // - OpenHands has no named providers or model list (no profile is used by the CLI), so the LLM entry itself is switched.
28
+ // - With max_input_tokens and max_output_tokens null, the SDK looks the model up in LiteLLM's catalog, under the id and
29
+ // under the part after the last "/" (sdk/llm/utils/model_info.py get_litellm_model_info): gemini-3.8-flash is there
30
+ // (1,048,576 in, 65,536 out, images). 100xprompt Auto's id is not, so for auto both are set to what every model auto may
31
+ // serve takes (common.ts limitsFor). Images: OpenHands decides them from that same catalog only (llm.py
32
+ // _supports_vision; there is no setting to turn them on), so with auto it sends no images.
28
33
  import { readFileSync } from "node:fs"
29
34
  import { join } from "node:path"
30
- import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type GatewaySettings, type Json, readKey, routeUrl } from "../common.ts"
35
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type GatewaySettings, isAuto, type Json, limitsFor, readKey, routeUrl } from "../common.ts"
31
36
 
32
37
  // ---- JSON edited in place ------------------------------------------------------------------------------------------------
33
38
 
@@ -261,8 +266,8 @@ const llmValues = (prefix: string, home: string, s: GatewaySettings): Record<str
261
266
  [`${prefix}/api_key`]: readKey(home),
262
267
  [`${prefix}/extra_headers`]: s.contextEdit ? { [EDIT_HEADER]: "on" } : null,
263
268
  [`${prefix}/model_canonical_name`]: null,
264
- [`${prefix}/max_input_tokens`]: null,
265
- [`${prefix}/max_output_tokens`]: null,
269
+ [`${prefix}/max_input_tokens`]: isAuto(s) ? limitsFor(s).context : null,
270
+ [`${prefix}/max_output_tokens`]: isAuto(s) ? limitsFor(s).output : null,
266
271
  [`${prefix}/litellm_extra_body`]: {},
267
272
  })
268
273
 
@@ -289,7 +294,7 @@ export const connectors: Connector[] = [
289
294
  },
290
295
  notes: [
291
296
  "OpenHands keeps the key in its agent_settings.json (it has no key-file option), so the file is now readable by you only (0600); a new sign-in replaces the key there.",
292
- "Its agent and memory-condenser LLM now use the gateway's model over OpenAI Chat Completions; the model limits OpenHands had stored for your previous model are cleared so it looks them up again.",
297
+ "Its agent and memory-condenser LLM now use the gateway's model over OpenAI Chat Completions; the model limits OpenHands had stored for your previous model are cleared so it looks them up again (with 100xprompt Auto they are set instead, and OpenHands sends no images: it decides that from the model's name alone).",
293
298
  "Run without --override-with-envs (or without LLM_API_KEY, LLM_BASE_URL and LLM_MODEL set): those override this file. Changing the LLM in OpenHands' settings screen replaces these values, and disconnect then leaves your choice alone.",
294
299
  ],
295
300
  },
@@ -20,13 +20,18 @@
20
20
  // under a name of its own (BRAIN_GATEWAY_KEY, so no OPENAI_API_KEY of the person's is touched), and that file is 0600.
21
21
  // - Custom headers: generationConfig.customHeaders is sent with every request (chunks/chunk-ESPAP3UW.js buildHeaders),
22
22
  // so `x-100x-context-edit: on` goes there.
23
+ // - Context window and input kinds: Qwen Code takes them from the model id (chunks/chunk-5TODR7F6.js tokenLimit and
24
+ // defaultModalities: gemini-* gets 1,000,000 tokens and every input kind; an id it does not know gets 200,000 and text
25
+ // only), unless the provider entry's generationConfig gives contextWindowSize and modalities (chunks/chunk-KUBWE5YV.js
26
+ // ModelRegistry, MODEL_GENERATION_CONFIG_FIELDS). 100xprompt Auto's id is one it cannot know, so for auto they are given:
27
+ // what every model auto may serve takes (common.ts limitsFor), images and text.
23
28
  // What connect writes: settings.json gets a "brain-gateway" provider ("100xprompt gateway", model shown as its 100xprompt
24
29
  // name), its providerProtocol mapping, and the selection (auth type, model.name, model.baseUrl); ~/.qwen/.env gets the
25
30
  // key. The person's own providers, models and comments are left as they are; disconnect puts every key back.
26
31
  // Needs Qwen Code 0.23 or later (custom provider ids through providerProtocol); older ones skip the provider.
27
32
  import { existsSync } from "node:fs"
28
33
  import { join } from "node:path"
29
- import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, modelName, readKey, routeUrl } from "../common.ts"
34
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, isAuto, type Json, limitsFor, modelName, readKey, routeUrl } from "../common.ts"
30
35
 
31
36
  // ---- JSON with comments, edited in place ----------------------------------------------------------------------------
32
37
  //
@@ -372,6 +377,15 @@ export function qwenDir(home: string, env: Env): string {
372
377
 
373
378
  const base = (s: GatewaySettings) => `${routeUrl(s)}/v1`
374
379
 
380
+ /** The provider entry's generationConfig: the context-edit header, and for auto the window and input kinds (see above). */
381
+ function generationConfig(s: GatewaySettings): Json | undefined {
382
+ const g: Json = {
383
+ ...(s.contextEdit ? { customHeaders: { [EDIT_HEADER]: "on" } } : {}),
384
+ ...(isAuto(s) ? { contextWindowSize: limitsFor(s).context, modalities: { image: true } } : {}),
385
+ }
386
+ return Object.keys(g).length ? g : undefined
387
+ }
388
+
375
389
  export const connectors: Connector[] = [
376
390
  {
377
391
  id: "qwen-code",
@@ -392,7 +406,7 @@ export const connectors: Connector[] = [
392
406
  description: "100xprompt gateway",
393
407
  baseUrl: base(s),
394
408
  envKey: QWEN_KEY_VAR,
395
- ...(s.contextEdit ? { generationConfig: { customHeaders: { [EDIT_HEADER]: "on" } } } : {}),
409
+ ...(generationConfig(s) ? { generationConfig: generationConfig(s) } : {}),
396
410
  },
397
411
  ],
398
412
  [`providerProtocol/${PROVIDER}`]: "openai",
@@ -27,13 +27,10 @@
27
27
  // Only the default VS Code profile and VS Code Stable ("Code") are handled; Insiders, VSCodium and other profiles keep
28
28
  // their own User folders.
29
29
  import { join, resolve } from "node:path"
30
- import { type Codec, type Connector, EDIT_HEADER, type Env, type Json, modelName, readKey, routeUrl } from "../common.ts"
30
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type Json, limitsFor, modelName, readKey, routeUrl } from "../common.ts"
31
31
 
32
32
  const GROUP = "100xprompt gateway"
33
33
  const VENDOR = "customendpoint"
34
- /** Gemini 3.x Flash, which the gateway's models are: a 1M-token window, 64K out. */
35
- const MAX_OUTPUT = 65_536
36
- const MAX_INPUT = 1_048_576 - MAX_OUTPUT
37
34
 
38
35
  /** VS Code's user folder: ~/Library/Application Support/Code/User on macOS, $XDG_CONFIG_HOME/Code/User or ~/.config/Code/User elsewhere. */
39
36
  export function vscodeUserDir(home: string, env: Env, platform: string = process.platform): string {
@@ -268,8 +265,9 @@ export const connectors: Connector[] = [
268
265
  apiType: "chat-completions",
269
266
  toolCalling: true,
270
267
  vision: true,
271
- maxInputTokens: MAX_INPUT,
272
- maxOutputTokens: MAX_OUTPUT,
268
+ // The window less the output (Gemini 3.x Flash: 1M in, 64K out; 100xprompt Auto: what all it may serve take).
269
+ maxInputTokens: limitsFor(s).context - limitsFor(s).output,
270
+ maxOutputTokens: limitsFor(s).output,
273
271
  requestHeaders: { Authorization: `Bearer ${readKey(home)}`, ...(s.contextEdit ? { [EDIT_HEADER]: "on" } : {}) },
274
272
  },
275
273
  ],