@atmorouter/helper 0.2.1 → 0.2.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 +26 -12
- package/bin/atmorouter-helper.mjs +13 -5
- package/lib/helper.mjs +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Configure coding agents for AtmoRouter, get IDE setup instructions, discover model IDs, and check a key without running inference. Changes are previewed first; automatic configuration merges existing settings and creates backups.
|
|
4
4
|
|
|
5
|
-
Requires Node.js 22 or newer. The
|
|
5
|
+
Requires Node.js 22 or newer. The package uses the `yaml` runtime dependency to safely parse and preserve YAML configuration. No repository access is required. Version 0.2.3 adds post-setup budget, routing-wait, and retry guidance while keeping generated configuration and shell output clean. The commands below pin that version; the versioned website archive is an alternative installation channel.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx @atmorouter/helper@0.2.
|
|
9
|
-
npx @atmorouter/helper@0.2.
|
|
10
|
-
npx @atmorouter/helper@0.2.
|
|
11
|
-
npx @atmorouter/helper@0.2.
|
|
12
|
-
npx @atmorouter/helper@0.2.
|
|
8
|
+
npx @atmorouter/helper@0.2.3 --help
|
|
9
|
+
npx @atmorouter/helper@0.2.3 targets
|
|
10
|
+
npx @atmorouter/helper@0.2.3 models --search claude
|
|
11
|
+
npx @atmorouter/helper@0.2.3 configure --target opencode --model YOUR_MODEL_ID
|
|
12
|
+
npx @atmorouter/helper@0.2.3 configure --target claude --model YOUR_MODEL_ID --write
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The same version is available as `https://atmorouter.dev/downloads/atmorouter-helper-0.2.
|
|
15
|
+
The same version is available as `https://atmorouter.dev/downloads/atmorouter-helper-0.2.3.tgz`, with a `.sha256` checksum alongside it. Run it with `npx https://atmorouter.dev/downloads/atmorouter-helper-0.2.3.tgz --help`.
|
|
16
16
|
|
|
17
17
|
With no command, the interactive wizard searches live text models and previews the selected setup. `configure` is preview-only unless `--write` is supplied. Non-interactive file writes additionally require `--yes`. Tests use temporary `--config-dir` folders, never real agent settings.
|
|
18
18
|
|
|
@@ -36,16 +36,30 @@ Guided targets print the steps to perform in your installed client; they do not
|
|
|
36
36
|
|
|
37
37
|
Use `--shell powershell` for PowerShell snippets. `--config-dir PATH` explicitly selects a settings folder. `--offline` skips live model validation; it does not establish model availability or client compatibility. Preview output shows intended changes, not existing file contents.
|
|
38
38
|
|
|
39
|
+
## Budgets after setup
|
|
40
|
+
|
|
41
|
+
After an automatic setup, preview, manual IDE integration, or bridge launch, open [Budgets](https://atmorouter.dev/dashboard/budgets). Select the actual API key used by that client, then choose an all-model percentage rule for broad coverage. Illustrative examples only: 20% below official rates or 50% below official rates. Review the displayed rates and choose your own limit. A family or exact-model rule covers only that target. Account and key rules both apply; the strictest input and output limits win. The helper never saves or raises budget rules automatically.
|
|
42
|
+
|
|
43
|
+
Market prices and tiers vary. A “From” price is a snapshot of the cheapest advertised offer, not a guaranteed execution price. Setup and key checks do not prove current availability within your limits. The CLI sends these reminders to stderr so piping an environment snippet from stdout still produces valid shell commands. Generated configuration files contain client settings only.
|
|
44
|
+
|
|
45
|
+
## Waiting and retries in your harness
|
|
46
|
+
|
|
47
|
+
Routing can wait up to 30 seconds for capacity within your caps. This wait is fixed; generation time is additional. Set the client timeout to allow both routing and the expected generation duration. The local bridge defaults to a 120-second total timeout, including routing and generation; a caller using the bridge must allow for that total duration.
|
|
48
|
+
|
|
49
|
+
For HTTP 429 or temporary availability failures, honor `Retry-After` when present. Otherwise use bounded exponential backoff with jitter. Avoid blind retries and duplicate in-flight requests. After a timeout or interrupted stream, review the client outcome and usage before deciding to resend; a lost response does not establish that the request was unprocessed. The bridge does not automatically retry inference or raise price caps.
|
|
50
|
+
|
|
51
|
+
Correct invalid requests before retrying. For authentication errors, verify the key and account eligibility. For insufficient balance, review Billing. For `balance_temporarily_reserved`, allow existing requests to finish. For `price_limit_unavailable`, wait for capacity within your caps or deliberately review your budget. Adding funds does not resolve pricing availability.
|
|
52
|
+
|
|
39
53
|
## Credentials
|
|
40
54
|
|
|
41
55
|
The helper never saves or prints your API key. Set `ATMOROUTER_API_KEY` privately through your shell or secret manager. OpenCode reads `{env:ATMOROUTER_API_KEY}`; Oh My Pi reads the environment variable named by `apiKey`. Continue uses `${{ secrets.ATMOROUTER_API_KEY }}`; for the IDE, manually add the secret to `~/.continue/.env` without committing that file. For Claude Code, set `ANTHROPIC_AUTH_TOKEN` to the same key before launching Claude. Guided IDE targets require you to enter the key in the client's own credential UI.
|
|
42
56
|
|
|
43
57
|
```sh
|
|
44
58
|
export ANTHROPIC_AUTH_TOKEN="$ATMOROUTER_API_KEY"
|
|
45
|
-
npx @atmorouter/helper@0.2.
|
|
59
|
+
npx @atmorouter/helper@0.2.3 check-key
|
|
46
60
|
```
|
|
47
61
|
|
|
48
|
-
`GET /v1/models` is public and **cannot validate a key**. The optional explicit key check sends an empty JSON body to `/v1/chat/completions`. Only HTTP 400 with `model is required` proves the gateway accepted the credential before rejecting the body. No model is selected, no inference is forwarded, and nothing is billed. The check consumes a rate-limit slot. HTTP 401 rejects the key; 429 and all other responses are inconclusive.
|
|
62
|
+
`GET /v1/models` is public and **cannot validate a key**. The optional explicit key check sends an empty JSON body to `/v1/chat/completions`. Only HTTP 400 with `model is required` proves the gateway accepted the credential before rejecting the body. No model is selected, no inference is forwarded, and nothing is billed. The check consumes a rate-limit slot. HTTP 401 rejects the key; 429 and all other responses are inconclusive. The key-check request has a 15-second timeout and refuse redirects. Catalog discovery has a 30-second timeout. No custom upstream origin can receive your key.
|
|
49
63
|
|
|
50
64
|
## Codex local bridge
|
|
51
65
|
|
|
@@ -53,13 +67,13 @@ The public gateway implements Chat Completions and Anthropic Messages, not `/v1/
|
|
|
53
67
|
|
|
54
68
|
```sh
|
|
55
69
|
# Prints instructions only; does not start a server or write Codex settings.
|
|
56
|
-
npx @atmorouter/helper@0.2.
|
|
70
|
+
npx @atmorouter/helper@0.2.3 configure --target codex --model YOUR_MODEL_ID
|
|
57
71
|
|
|
58
72
|
# Starts a temporary local bridge and launches your installed Codex CLI.
|
|
59
|
-
npx @atmorouter/helper@0.2.
|
|
73
|
+
npx @atmorouter/helper@0.2.3 codex --model YOUR_MODEL_ID
|
|
60
74
|
|
|
61
75
|
# Or run the bridge separately and follow its printed client instructions.
|
|
62
|
-
npx @atmorouter/helper@0.2.
|
|
76
|
+
npx @atmorouter/helper@0.2.3 bridge --port 43187
|
|
63
77
|
```
|
|
64
78
|
|
|
65
79
|
Set `ATMOROUTER_API_KEY` in the process environment before starting the bridge. It listens on loopback only, authenticates local requests, and forwards inference to the official AtmoRouter endpoint. The launcher preserves Codex's permission defaults. It does not weaken approvals or sandbox settings.
|
|
@@ -5,9 +5,14 @@ import {readFile} from 'node:fs/promises';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {spawn} from 'node:child_process';
|
|
8
|
-
import {
|
|
8
|
+
import {setDefaultResultOrder} from 'node:dns';
|
|
9
|
+
import {models,modelId,checkKey,planConfiguration,writeConfiguration,environment,INTEGRATIONS,guidedConfiguration,SETUP_GUIDANCE} from '../lib/helper.mjs';
|
|
9
10
|
import {startResponsesBridge} from '../lib/bridge.mjs';
|
|
10
11
|
|
|
12
|
+
// Prefer IPv4 when both families are advertised; IPv6-only DNS still works.
|
|
13
|
+
// This affects this CLI process only, including the bridge's outbound requests.
|
|
14
|
+
setDefaultResultOrder('ipv4first');
|
|
15
|
+
|
|
11
16
|
const HELP=`AtmoRouter helper
|
|
12
17
|
Node.js 22+.
|
|
13
18
|
|
|
@@ -54,6 +59,7 @@ async function launchCodex(model,args){
|
|
|
54
59
|
const bridge=await startResponsesBridge({port:0});
|
|
55
60
|
try {
|
|
56
61
|
console.log('Starting Codex through a loopback-only bridge. Hosted web search is disabled for this run. Model requests are billed by AtmoRouter.');
|
|
62
|
+
console.error(SETUP_GUIDANCE);
|
|
57
63
|
const configuration=['-c','model_provider="atmorouter"','-c','model='+JSON.stringify(model),'-c',`model_providers.atmorouter={name="AtmoRouter",base_url="${bridge.url}",env_key="ATMOROUTER_API_KEY",wire_api="responses",requires_openai_auth=false,supports_websockets=false}`,'-c','web_search="disabled"'];
|
|
58
64
|
const code=await new Promise((resolve,reject)=>{
|
|
59
65
|
const child=spawn(executable.file,[...executable.args,...configuration,...args],{stdio:'inherit',env:process.env});
|
|
@@ -79,6 +85,7 @@ async function main(){
|
|
|
79
85
|
if(command==='bridge'){
|
|
80
86
|
if(!/^\d+$/.test(values.port)||Number(values.port)<1||Number(values.port)>65535)throw Error('Bridge port must be an integer from 1 to 65535.');
|
|
81
87
|
const bridge=await startResponsesBridge({port:Number(values.port)});
|
|
88
|
+
console.error(SETUP_GUIDANCE);
|
|
82
89
|
console.log('Local Responses bridge: '+bridge.url+'\nStateless text/tools only; normal AtmoRouter charges apply. Press Ctrl+C to stop.');
|
|
83
90
|
console.log('In your Codex configuration, set model_provider = "atmorouter" and your chosen model and web_search = "disabled" before provider tables. Then add:\n\n[model_providers.atmorouter]\nname = "AtmoRouter local bridge"\nbase_url = "'+bridge.url+'"\nenv_key = "ATMOROUTER_API_KEY"\nwire_api = "responses"\nrequires_openai_auth = false\nsupports_websockets = false\n\nSet ATMOROUTER_API_KEY privately in the terminal that starts Codex. Run configure --target codex --model YOUR_MODEL_ID for full instructions.');
|
|
84
91
|
await new Promise(resolve=>{process.once('SIGINT',resolve);process.once('SIGTERM',resolve);});await bridge.close();return;
|
|
@@ -113,12 +120,12 @@ async function main(){
|
|
|
113
120
|
if(integration.mode==='unavailable')throw Error(guidedConfiguration(target,model,{shell:values.shell}));
|
|
114
121
|
if(['guided','bridge'].includes(integration.mode)){
|
|
115
122
|
if(values.write)throw Error('This integration uses guided setup. No configuration files were changed.');
|
|
116
|
-
console.log(guidedConfiguration(target,model,{shell:values.shell}));return;
|
|
123
|
+
console.log(guidedConfiguration(target,model,{shell:values.shell}));console.error(SETUP_GUIDANCE);return;
|
|
117
124
|
}
|
|
118
125
|
if(!values.offline&&!rl){if(!(await models()).some(m=>m.id===model))throw Error('Model is not in the live text catalog. Use models to choose one.');}
|
|
119
126
|
if(['openai','anthropic'].includes(target)){
|
|
120
127
|
if(values.write)throw Error('Environment snippets are preview-only. Copy them into your shell.');
|
|
121
|
-
console.log(environment(target,model,values.shell));return;
|
|
128
|
+
console.log(environment(target,model,values.shell));console.error(SETUP_GUIDANCE);return;
|
|
122
129
|
}
|
|
123
130
|
const plan=await planConfiguration(target,model,{directory:values['config-dir']});
|
|
124
131
|
// Print only the intended patch. Existing settings may contain secrets.
|
|
@@ -131,13 +138,14 @@ async function main(){
|
|
|
131
138
|
if(values.yes)apply=true;
|
|
132
139
|
else {if(!process.stdin.isTTY)throw Error('Non-interactive writes require --write --yes. Nothing changed.');rl=createInterface({input:process.stdin,output:process.stdout});apply=/^y(es)?$/i.test((await rl.question('Apply these changes? [y/N] ')).trim());}
|
|
133
140
|
}
|
|
134
|
-
if(!apply){console.log('Preview only. No files changed.');return;}
|
|
141
|
+
if(!apply){console.log('Preview only. No files changed.');console.error(SETUP_GUIDANCE);return;}
|
|
135
142
|
const written=await writeConfiguration(plan);console.log('Saved: '+written.file);if(written.backup)console.log('Backup: '+written.backup);
|
|
143
|
+
console.error(SETUP_GUIDANCE);
|
|
136
144
|
} finally {rl?.close();}
|
|
137
145
|
}
|
|
138
146
|
main().catch(error=>{
|
|
139
147
|
let message=String(error?.message || 'Operation failed.');
|
|
140
|
-
if(error?.code
|
|
148
|
+
if(typeof error?.code==='string'&&error.code.startsWith('ERR_PARSE_ARGS'))message='Unknown or unsupported command options. Do not pass API keys as arguments. Run --help.';
|
|
141
149
|
else if(error instanceof SyntaxError)message='Unexpected catalog JSON. Try again later.';
|
|
142
150
|
else if(error instanceof TypeError || error?.name==='TimeoutError')message='Network operation failed. Check connectivity and retry.';
|
|
143
151
|
if(process.env.ATMOROUTER_API_KEY)message=message.split(process.env.ATMOROUTER_API_KEY).join('[redacted]');
|
package/lib/helper.mjs
CHANGED
|
@@ -6,13 +6,19 @@ import {planYamlConfiguration,assertSafeConfigurationPath} from './integrations.
|
|
|
6
6
|
export {INTEGRATIONS,guidedConfiguration} from './integrations.mjs';
|
|
7
7
|
|
|
8
8
|
export const ORIGIN = 'https://atmorouter.dev';
|
|
9
|
+
export const SETUP_GUIDANCE = `Before using your client: review Budgets at https://atmorouter.dev/dashboard/budgets.
|
|
10
|
+
Market prices and tiers vary. A "From" price is a snapshot of the cheapest advertised offer, not a guaranteed execution price.
|
|
11
|
+
Select the actual API key used by this client and an all-model percentage rule for broad coverage. Illustrative examples only: 20% below official rates, or 50% below official rates; choose your own limit after reviewing the rates. Account and key rules both apply; the strictest input and output limits win. A rule for one model does not cover other models. The helper never saves or raises budget rules automatically.
|
|
12
|
+
Routing can wait up to 30 seconds for capacity within your caps. This wait is fixed; generation time is additional. Allow both in your client timeout. The local bridge has a 120-second total timeout by default.
|
|
13
|
+
For 429 or temporary availability failures, honor Retry-After when present; otherwise use bounded exponential backoff with jitter. Do not retry blindly or duplicate an in-flight request. After a timeout or interrupted stream, review the client outcome and usage before deciding to resend. Never raise price caps automatically.
|
|
14
|
+
For invalid requests, correct the request; for authentication errors, check the key and account eligibility; for insufficient balance, review Billing. A price_limit_unavailable refusal needs capacity within your caps or a deliberate budget review; adding funds does not fix pricing availability. Client setup and key checks do not prove current availability within your limits.`;
|
|
9
15
|
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
16
|
export function modelId(value) {
|
|
11
17
|
if (typeof value!=='string' || value!==value.trim() || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,200}$/.test(value)) throw Error('Choose a model ID from the catalog; shell characters and whitespace are not allowed.');
|
|
12
18
|
return value;
|
|
13
19
|
}
|
|
14
20
|
export async function models(fetcher = fetch) {
|
|
15
|
-
const response = await fetcher(ORIGIN+'/v1/models', {redirect:'error',signal:AbortSignal.timeout(
|
|
21
|
+
const response = await fetcher(ORIGIN+'/v1/models', {redirect:'error',signal:AbortSignal.timeout(30000)});
|
|
16
22
|
if (!response.ok) throw Error('Catalog request failed: HTTP '+response.status);
|
|
17
23
|
const body = await response.json();
|
|
18
24
|
if (!Array.isArray(body.data)) throw Error('Unexpected catalog response.');
|
package/package.json
CHANGED