opencode-azure-setup 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/index.js +119 -41
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -33,13 +33,10 @@ const logo = `
33
33
  |_| Azure Edition
34
34
  `;
35
35
 
36
- // Get config path
37
36
  function getConfigPath() {
38
- const home = os.homedir();
39
- return path.join(home, '.config', 'opencode', 'opencode.json');
37
+ return path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
40
38
  }
41
39
 
42
- // Readline interface
43
40
  const rl = readline.createInterface({
44
41
  input: process.stdin,
45
42
  output: process.stdout,
@@ -55,13 +52,11 @@ function ask(question, defaultValue = '') {
55
52
  function askPassword(question) {
56
53
  return new Promise((resolve) => {
57
54
  process.stdout.write(`${question}: `);
58
-
59
55
  if (process.stdin.isTTY) {
60
56
  const stdin = process.stdin;
61
57
  stdin.setRawMode(true);
62
58
  stdin.resume();
63
59
  stdin.setEncoding('utf8');
64
-
65
60
  let password = '';
66
61
  const onData = (char) => {
67
62
  if (char === '\n' || char === '\r' || char === '\u0004') {
@@ -84,13 +79,84 @@ function askPassword(question) {
84
79
  });
85
80
  }
86
81
 
87
- async function testConnection(endpoint, apiKey, deployment) {
88
- return new Promise((resolve) => {
89
- const url = new URL(`${endpoint}/deployments/${deployment}/chat/completions?api-version=2024-12-01-preview`);
82
+ // Fetch latest defaults from GitHub (falls back to hardcoded if offline)
83
+ async function fetchDefaults() {
84
+ const defaults = {
85
+ deployment: 'model-router',
86
+ apiVersion: '2025-01-01-preview',
87
+ };
88
+
89
+ try {
90
+ const res = await fetch('https://raw.githubusercontent.com/schwarztim/opencode/dev/azure-defaults.json', {
91
+ signal: AbortSignal.timeout(3000),
92
+ });
93
+ if (res.ok) {
94
+ const data = await res.json();
95
+ if (data.apiVersion) defaults.apiVersion = data.apiVersion;
96
+ if (data.deployment) defaults.deployment = data.deployment;
97
+ }
98
+ } catch {
99
+ // Offline or fetch failed - use hardcoded defaults
100
+ }
101
+
102
+ return defaults;
103
+ }
104
+
105
+ // Parse Azure endpoint - handles both full URL and base URL
106
+ function parseAzureEndpoint(input, defaults) {
107
+ const result = {
108
+ baseUrl: '',
109
+ deployment: defaults.deployment,
110
+ apiVersion: defaults.apiVersion,
111
+ };
112
+
113
+ try {
114
+ const url = new URL(input);
115
+
116
+ // Extract deployment from path: /openai/deployments/{deployment}/...
117
+ const deploymentMatch = url.pathname.match(/\/deployments\/([^/]+)/);
118
+ if (deploymentMatch) {
119
+ result.deployment = deploymentMatch[1];
120
+ }
121
+
122
+ // Extract api-version from query params
123
+ const apiVersion = url.searchParams.get('api-version');
124
+ if (apiVersion) {
125
+ result.apiVersion = apiVersion;
126
+ }
127
+
128
+ // Build base URL: https://host/openai
129
+ const pathParts = url.pathname.split('/');
130
+ const openaiIndex = pathParts.indexOf('openai');
131
+ if (openaiIndex !== -1) {
132
+ url.pathname = pathParts.slice(0, openaiIndex + 1).join('/');
133
+ } else {
134
+ url.pathname = '/openai';
135
+ }
136
+ url.search = '';
137
+ result.baseUrl = url.toString().replace(/\/$/, '');
138
+
139
+ } catch {
140
+ // Not a valid URL, assume it's just the host
141
+ let cleaned = input.replace(/\/$/, '');
142
+ if (!cleaned.startsWith('https://')) {
143
+ cleaned = 'https://' + cleaned;
144
+ }
145
+ if (!cleaned.endsWith('/openai')) {
146
+ cleaned += '/openai';
147
+ }
148
+ result.baseUrl = cleaned;
149
+ }
150
+
151
+ return result;
152
+ }
90
153
 
154
+ async function testConnection(endpoint, apiKey, deployment, apiVersion) {
155
+ return new Promise((resolve) => {
156
+ const url = new URL(`${endpoint}/deployments/${deployment}/chat/completions?api-version=${apiVersion}`);
91
157
  const options = {
92
158
  hostname: url.hostname,
93
- port: url.port || 443,
159
+ port: 443,
94
160
  path: url.pathname + url.search,
95
161
  method: 'POST',
96
162
  headers: {
@@ -111,7 +177,7 @@ async function testConnection(endpoint, apiKey, deployment) {
111
177
  });
112
178
 
113
179
  req.on('error', (e) => resolve({ ok: false, status: 0, body: e.message }));
114
- req.setTimeout(10000, () => {
180
+ req.setTimeout(15000, () => {
115
181
  req.destroy();
116
182
  resolve({ ok: false, status: 0, body: 'Timeout' });
117
183
  });
@@ -127,39 +193,40 @@ async function main() {
127
193
  console.log('─'.repeat(40));
128
194
  console.log();
129
195
 
130
- // Endpoint
131
- console.log('Enter your Azure OpenAI endpoint');
132
- console.log(colors.dim + '(from Azure Portal → Azure OpenAI → Keys and Endpoint)' + colors.reset);
133
- let endpoint = await ask('Endpoint');
196
+ // Fetch latest defaults (non-blocking, falls back to hardcoded)
197
+ const defaults = await fetchDefaults();
134
198
 
135
- if (!endpoint) {
199
+ // Endpoint - accepts full URL or just the base
200
+ console.log('Paste your Azure OpenAI endpoint');
201
+ console.log(colors.dim + 'Tip: You can paste the full URL from Azure Portal - we\'ll extract what we need' + colors.reset);
202
+ console.log();
203
+ const rawEndpoint = await ask('Endpoint');
204
+
205
+ if (!rawEndpoint) {
136
206
  console.log(colors.red + 'Endpoint is required' + colors.reset);
137
207
  process.exit(1);
138
208
  }
139
209
 
140
- endpoint = endpoint.replace(/\/$/, '');
141
- if (!endpoint.endsWith('/openai')) endpoint += '/openai';
142
-
143
- console.log();
210
+ // Parse the endpoint - extracts base URL, deployment, and api-version automatically
211
+ const parsed = parseAzureEndpoint(rawEndpoint, defaults);
144
212
 
145
213
  // API Key
214
+ console.log();
146
215
  const apiKey = await askPassword('API Key');
147
216
  if (!apiKey) {
148
217
  console.log(colors.red + 'API Key is required' + colors.reset);
149
218
  process.exit(1);
150
219
  }
151
220
 
152
- console.log();
153
-
154
- // Deployment
155
- console.log('Enter your deployment name');
156
- console.log(colors.dim + '(default: model-router for Azure APIM setups)' + colors.reset);
157
- const deployment = await ask('Deployment', 'model-router');
221
+ // Use auto-detected values
222
+ let deployment = parsed.deployment;
223
+ let apiVersion = parsed.apiVersion;
158
224
 
159
225
  console.log();
160
226
  console.log(colors.blue + 'Testing connection...' + colors.reset);
227
+ console.log(colors.dim + ` ${parsed.baseUrl}/deployments/${deployment}` + colors.reset);
161
228
 
162
- const result = await testConnection(endpoint, apiKey, deployment);
229
+ let result = await testConnection(parsed.baseUrl, apiKey, deployment, apiVersion);
163
230
 
164
231
  if (result.ok) {
165
232
  console.log(colors.green + '✓ Connection successful!' + colors.reset);
@@ -173,16 +240,29 @@ async function main() {
173
240
  console.log(colors.dim + result.body.slice(0, 200) + colors.reset);
174
241
  }
175
242
  }
243
+
244
+ // Offer to edit settings if connection failed
176
245
  console.log();
177
- const cont = await ask('Continue anyway? (y/N)', 'N');
178
- if (cont.toLowerCase() !== 'y') process.exit(1);
246
+ console.log(colors.yellow + 'Let\'s try different settings:' + colors.reset);
247
+ deployment = await ask('Deployment name', deployment);
248
+ apiVersion = await ask('API Version', apiVersion);
249
+
250
+ console.log();
251
+ console.log(colors.blue + 'Retrying...' + colors.reset);
252
+ result = await testConnection(parsed.baseUrl, apiKey, deployment, apiVersion);
253
+
254
+ if (result.ok) {
255
+ console.log(colors.green + '✓ Connection successful!' + colors.reset);
256
+ } else {
257
+ console.log(colors.red + `✗ Still failing (${result.status || 'error'})` + colors.reset);
258
+ const cont = await ask('Save config anyway? (y/N)', 'N');
259
+ if (cont.toLowerCase() !== 'y') process.exit(1);
260
+ }
179
261
  }
180
262
 
181
263
  // Create config
182
264
  const configPath = getConfigPath();
183
- const configDir = path.dirname(configPath);
184
-
185
- fs.mkdirSync(configDir, { recursive: true });
265
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
186
266
 
187
267
  const config = {
188
268
  $schema: 'https://opencode.ai/config.json',
@@ -192,10 +272,10 @@ async function main() {
192
272
  npm: '@ai-sdk/azure',
193
273
  name: 'Azure OpenAI',
194
274
  options: {
195
- baseURL: endpoint,
275
+ baseURL: parsed.baseUrl,
196
276
  apiKey: apiKey,
197
277
  useDeploymentBasedUrls: true,
198
- apiVersion: '2024-12-01-preview',
278
+ apiVersion: apiVersion,
199
279
  },
200
280
  models: {
201
281
  [deployment]: {
@@ -210,15 +290,13 @@ async function main() {
210
290
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
211
291
 
212
292
  console.log();
213
- console.log(colors.green + `✓ Configuration saved to ${configPath}` + colors.reset);
293
+ console.log(colors.green + '✓ Configuration saved!' + colors.reset);
294
+ console.log(colors.dim + ` ${configPath}` + colors.reset);
214
295
  console.log();
215
- console.log(colors.blue + "You're all set! Run:" + colors.reset);
216
- console.log();
217
- console.log(' opencode');
296
+ console.log('─'.repeat(40));
297
+ console.log(colors.green + 'You\'re all set! Run:' + colors.reset);
218
298
  console.log();
219
- console.log(colors.dim + 'Tips:' + colors.reset);
220
- console.log(' • View config: opencode azure status');
221
- console.log(' • Reconfigure: opencode azure');
299
+ console.log(' ' + colors.blue + 'opencode' + colors.reset);
222
300
  console.log();
223
301
 
224
302
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-azure-setup",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Easy Azure OpenAI setup for OpenCode",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -22,7 +22,7 @@
22
22
  "license": "MIT",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/schwarztim/opencode.git"
25
+ "url": "git+https://github.com/schwarztim/opencode.git"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=18"