subconscious-cli 4.1.4 → 5.0.0
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 +3 -3
- package/bin/agents.js +1 -0
- package/bin/auth.js +131 -263
- package/bin/cli.js +15 -3
- package/bin/models.js +51 -9
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +61 -0
- package/bin/runbook/claude-code/run.sh +5 -0
- package/bin/runbook/opencode/install.sh +10 -34
- package/bin/runbook/opencode/provider.sh +93 -0
- package/bin/runbook/opencode/run.sh +3 -18
- package/bin/tui.js +247 -51
- package/bin/update-check.js +29 -8
- package/bin/usage.js +51 -19
- package/bin/windows/launch.js +4 -10
- package/package.json +1 -1
package/bin/models.js
CHANGED
|
@@ -18,6 +18,27 @@ export function isLiveModelSource(source) {
|
|
|
18
18
|
return source === 'available' || source === 'public';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function isAbortError(error) {
|
|
22
|
+
return error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function linkAbortSignal(controller, signal) {
|
|
26
|
+
if (!signal) return () => {};
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
controller.abort();
|
|
29
|
+
return () => {};
|
|
30
|
+
}
|
|
31
|
+
const onAbort = () => controller.abort();
|
|
32
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function abortError(message) {
|
|
37
|
+
const error = new Error(message);
|
|
38
|
+
error.name = 'AbortError';
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export function normalizeModelIds(modelIds = [], selectedModel) {
|
|
22
43
|
const models = [];
|
|
23
44
|
const seen = new Set();
|
|
@@ -53,6 +74,7 @@ export async function fetchGatewayModels({
|
|
|
53
74
|
path = PUBLIC_MODELS_PATH,
|
|
54
75
|
fetchImpl = globalThis.fetch,
|
|
55
76
|
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
77
|
+
signal,
|
|
56
78
|
}) {
|
|
57
79
|
if (typeof fetchImpl !== 'function') {
|
|
58
80
|
throw new Error('This Node.js version does not support fetch');
|
|
@@ -71,14 +93,26 @@ export async function fetchGatewayModels({
|
|
|
71
93
|
const controller = new AbortController();
|
|
72
94
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
73
95
|
timeout.unref?.();
|
|
96
|
+
const unlink = linkAbortSignal(controller, signal);
|
|
74
97
|
|
|
75
98
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
100
|
+
const aborted = new Promise((_, reject) => {
|
|
101
|
+
const fail = () => {
|
|
102
|
+
if (signal?.aborted) reject(abortError('Model discovery was cancelled'));
|
|
103
|
+
else reject(new Error(`Model discovery timed out after ${timeoutMs}ms`));
|
|
104
|
+
};
|
|
105
|
+
controller.signal.addEventListener('abort', fail, { once: true });
|
|
81
106
|
});
|
|
107
|
+
const response = await Promise.race([
|
|
108
|
+
fetchImpl(endpoint, {
|
|
109
|
+
method: 'GET',
|
|
110
|
+
headers,
|
|
111
|
+
cache: 'no-store',
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
}),
|
|
114
|
+
aborted,
|
|
115
|
+
]);
|
|
82
116
|
|
|
83
117
|
if (!response.ok) {
|
|
84
118
|
throw new Error(`Model discovery returned HTTP ${response.status}`);
|
|
@@ -91,21 +125,24 @@ export async function fetchGatewayModels({
|
|
|
91
125
|
|
|
92
126
|
return normalizeModelIds(payload.data.map((model) => model?.id));
|
|
93
127
|
} catch (error) {
|
|
128
|
+
if (signal?.aborted) throw abortError('Model discovery was cancelled');
|
|
94
129
|
if (controller.signal.aborted) {
|
|
95
130
|
throw new Error(`Model discovery timed out after ${timeoutMs}ms`);
|
|
96
131
|
}
|
|
97
132
|
throw error;
|
|
98
133
|
} finally {
|
|
134
|
+
unlink();
|
|
99
135
|
clearTimeout(timeout);
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
|
-
async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs }) {
|
|
139
|
+
async function fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal }) {
|
|
104
140
|
const models = await fetchGatewayModels({
|
|
105
141
|
baseUrl,
|
|
106
142
|
path: PUBLIC_MODELS_PATH,
|
|
107
143
|
fetchImpl,
|
|
108
144
|
timeoutMs,
|
|
145
|
+
signal,
|
|
109
146
|
});
|
|
110
147
|
if (!models.length) throw new Error('Model discovery returned no usable model IDs');
|
|
111
148
|
return models;
|
|
@@ -134,6 +171,7 @@ export async function resolveModelCatalog({
|
|
|
134
171
|
fallbackModels = [],
|
|
135
172
|
fetchImpl = globalThis.fetch,
|
|
136
173
|
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
174
|
+
signal,
|
|
137
175
|
}) {
|
|
138
176
|
const key = apiKey?.trim() || '';
|
|
139
177
|
|
|
@@ -145,22 +183,26 @@ export async function resolveModelCatalog({
|
|
|
145
183
|
path: AVAILABLE_MODELS_PATH,
|
|
146
184
|
fetchImpl,
|
|
147
185
|
timeoutMs,
|
|
186
|
+
signal,
|
|
148
187
|
});
|
|
149
188
|
return liveCatalog(discovered, selectedModel, 'available');
|
|
150
189
|
} catch (availableError) {
|
|
190
|
+
if (isAbortError(availableError) || signal?.aborted) throw availableError;
|
|
151
191
|
try {
|
|
152
|
-
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
|
|
192
|
+
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal });
|
|
153
193
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
154
|
-
} catch {
|
|
194
|
+
} catch (publicError) {
|
|
195
|
+
if (isAbortError(publicError) || signal?.aborted) throw publicError;
|
|
155
196
|
return packagedCatalog(selectedModel, fallbackModels, availableError);
|
|
156
197
|
}
|
|
157
198
|
}
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
try {
|
|
161
|
-
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs });
|
|
202
|
+
const discovered = await fetchPublicModels({ baseUrl, fetchImpl, timeoutMs, signal });
|
|
162
203
|
return liveCatalog(discovered, selectedModel, 'public');
|
|
163
204
|
} catch (error) {
|
|
205
|
+
if (isAbortError(error) || signal?.aborted) throw error;
|
|
164
206
|
return packagedCatalog(selectedModel, fallbackModels, error);
|
|
165
207
|
}
|
|
166
208
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { modelSupportsVision } from './model-capabilities.js';
|
|
2
|
+
|
|
3
|
+
export const OPENCODE_PROVIDER_ID = 'subconscious';
|
|
4
|
+
export const OPENCODE_PROVIDER_NAME = 'Subconscious Gateway';
|
|
5
|
+
|
|
6
|
+
const ACRONYMS = new Set(['gpt', 'oss', 'api', 'gguf', 'ggml', 'nomic', 'vl', 'it', 'mlx']);
|
|
7
|
+
|
|
8
|
+
function formatToken(token) {
|
|
9
|
+
const lower = token.toLowerCase();
|
|
10
|
+
if (ACRONYMS.has(lower)) return token.toUpperCase();
|
|
11
|
+
if (/^\d+[bkmg]$/i.test(token)) return token.toUpperCase();
|
|
12
|
+
if (/^q\d+$/i.test(token)) return token.toUpperCase();
|
|
13
|
+
if (/^\d+\.\d+/.test(token)) return token;
|
|
14
|
+
if (/^[a-z]\d+[a-z]$/i.test(token) || /^\d+[a-z]$/i.test(token)) return token.toUpperCase();
|
|
15
|
+
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Drop the subconscious/ prefix, then capitalize each dash-separated token. */
|
|
19
|
+
export function opencodeModelDisplayName(modelId) {
|
|
20
|
+
const text = String(modelId ?? '').trim();
|
|
21
|
+
const slug = text.startsWith('subconscious/') ? text.slice('subconscious/'.length) : text;
|
|
22
|
+
return slug.split(/[-_]/).filter(Boolean).map(formatToken).join(' ');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildOpenCodeModels(modelIds, { context, output }) {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
modelIds.map((id) => [
|
|
28
|
+
id,
|
|
29
|
+
{
|
|
30
|
+
name: opencodeModelDisplayName(id),
|
|
31
|
+
tools: true,
|
|
32
|
+
limit: { context, output },
|
|
33
|
+
...(modelSupportsVision(id)
|
|
34
|
+
? { attachment: true, modalities: { input: ['text', 'image'], output: ['text'] } }
|
|
35
|
+
: {}),
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildOpenCodeConfig({ baseUrl, model, modelIds, context, output }) {
|
|
42
|
+
return {
|
|
43
|
+
$schema: 'https://opencode.ai/config.json',
|
|
44
|
+
disabled_providers: ['subconscious-cli'],
|
|
45
|
+
provider: {
|
|
46
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
47
|
+
npm: '@ai-sdk/openai-compatible',
|
|
48
|
+
name: OPENCODE_PROVIDER_NAME,
|
|
49
|
+
whitelist: [...modelIds],
|
|
50
|
+
options: {
|
|
51
|
+
baseURL: `${baseUrl}/v1`,
|
|
52
|
+
apiKey: '{env:SUBCONSCIOUS_API_KEY}',
|
|
53
|
+
headers: { 'x-subconscious-client': 'opencode' },
|
|
54
|
+
modelsDiscovery: { enabled: false },
|
|
55
|
+
},
|
|
56
|
+
models: buildOpenCodeModels(modelIds, { context, output }),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
model: `${OPENCODE_PROVIDER_ID}/${model}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -216,6 +216,11 @@ export ANTHROPIC_DEFAULT_FABLE_MODEL="${ANTHROPIC_DEFAULT_FABLE_MODEL:-$FABLE_MO
|
|
|
216
216
|
export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="${ANTHROPIC_DEFAULT_FABLE_MODEL_NAME:-$FABLE_MODEL_NAME}"
|
|
217
217
|
export ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="${ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION:-$FABLE_MODEL_DESCRIPTION}"
|
|
218
218
|
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="${CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:-0}"
|
|
219
|
+
# Gateway upstreams can't perform Anthropic's server-side auto mode classifier
|
|
220
|
+
# checks; opting out avoids the "isn't eligible for no-charge classifier
|
|
221
|
+
# requests" fallback and keeps classifier requests billed locally.
|
|
222
|
+
# https://code.claude.com/docs/en/auto-mode-classifier-billing
|
|
223
|
+
export CLAUDE_CODE_AUTO_MODE_SERVER="${CLAUDE_CODE_AUTO_MODE_SERVER:-0}"
|
|
219
224
|
export CLAUDE_CODE_SUBAGENT_MODEL="${CLAUDE_CODE_SUBAGENT_MODEL:-$MODEL}"
|
|
220
225
|
export CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS}"
|
|
221
226
|
export CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH="${MAX_SUBAGENT_SPAWN_DEPTH}"
|
|
@@ -55,6 +55,7 @@ set -euo pipefail
|
|
|
55
55
|
|
|
56
56
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
57
57
|
source "${SCRIPT_DIR}/../model-capabilities.generated.sh"
|
|
58
|
+
source "${SCRIPT_DIR}/provider.sh"
|
|
58
59
|
|
|
59
60
|
# Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
|
|
60
61
|
SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
|
|
@@ -147,19 +148,7 @@ while IFS= read -r model_id; do
|
|
|
147
148
|
add_supported_model "$model_id"
|
|
148
149
|
done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
for model_id in "${SUPPORTED_MODELS[@]}"; do
|
|
152
|
-
vision_fields=""
|
|
153
|
-
if subc_model_supports_vision "$model_id"; then
|
|
154
|
-
vision_fields=',"attachment":true,"modalities":{"input":["text","image"],"output":["text"]}'
|
|
155
|
-
fi
|
|
156
|
-
model_json="\"${model_id}\":{\"name\":\"${model_id}\",\"tools\":true,\"limit\":{\"context\":${CONTEXT_LIMIT},\"output\":${OUTPUT_LIMIT}}${vision_fields}}"
|
|
157
|
-
if [[ -n "$MODELS_JSON" ]]; then
|
|
158
|
-
MODELS_JSON="${MODELS_JSON},${model_json}"
|
|
159
|
-
else
|
|
160
|
-
MODELS_JSON="$model_json"
|
|
161
|
-
fi
|
|
162
|
-
done
|
|
151
|
+
subc_opencode_build_models_json
|
|
163
152
|
|
|
164
153
|
OPENCODE_DIR="${HOME}/.opencode"
|
|
165
154
|
OPENCODE_CONFIG="${OPENCODE_DIR}/opencode.json"
|
|
@@ -186,34 +175,21 @@ require_cmds() {
|
|
|
186
175
|
write_config() {
|
|
187
176
|
mkdir -p "$OPENCODE_DIR"
|
|
188
177
|
local base_url="${GATEWAY_URL%/}/v1"
|
|
189
|
-
local provider
|
|
190
|
-
provider
|
|
191
|
-
{
|
|
192
|
-
"npm": "@ai-sdk/openai-compatible",
|
|
193
|
-
"name": "Subconscious Gateway",
|
|
194
|
-
"options": {
|
|
195
|
-
"baseURL": "${base_url}",
|
|
196
|
-
"apiKey": "{env:SUBCONSCIOUS_API_KEY}",
|
|
197
|
-
"headers": {
|
|
198
|
-
"x-subconscious-client": "opencode"
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
"models": {${MODELS_JSON}}
|
|
202
|
-
}
|
|
203
|
-
EOF
|
|
204
|
-
)
|
|
178
|
+
local provider selected_model
|
|
179
|
+
provider="$(subc_opencode_build_provider_json "$base_url")"
|
|
180
|
+
selected_model="${SUBC_OPENCODE_PROVIDER_ID}/${MODEL}"
|
|
205
181
|
if [[ -f "$OPENCODE_CONFIG" ]]; then
|
|
206
182
|
local tmp
|
|
207
183
|
tmp="$(mktemp)"
|
|
208
|
-
jq --argjson provider "$provider" --arg model "
|
|
184
|
+
jq --argjson provider "$provider" --arg model "$selected_model" --arg provider_id "$SUBC_OPENCODE_PROVIDER_ID" '
|
|
209
185
|
.provider = (.provider // {})
|
|
210
|
-
| .provider
|
|
186
|
+
| .provider[$provider_id] = $provider
|
|
211
187
|
| .model = $model
|
|
212
188
|
' "$OPENCODE_CONFIG" >"$tmp"
|
|
213
189
|
mv "$tmp" "$OPENCODE_CONFIG"
|
|
214
190
|
else
|
|
215
|
-
jq -n --argjson provider "$provider" --arg model "
|
|
216
|
-
'{ "$schema": "https://opencode.ai/config.json", provider: {
|
|
191
|
+
jq -n --argjson provider "$provider" --arg model "$selected_model" --arg provider_id "$SUBC_OPENCODE_PROVIDER_ID" \
|
|
192
|
+
'{ "$schema": "https://opencode.ai/config.json", provider: {($provider_id): $provider}, model: $model }' \
|
|
217
193
|
>"$OPENCODE_CONFIG"
|
|
218
194
|
fi
|
|
219
195
|
local env_file="${OPENCODE_DIR}/subconscious.env"
|
|
@@ -240,7 +216,7 @@ uninstall_config() {
|
|
|
240
216
|
local tmp
|
|
241
217
|
tmp="$(mktemp)"
|
|
242
218
|
jq '
|
|
243
|
-
del(.provider.subconscious)
|
|
219
|
+
del(.provider.subconscious, .provider["subconscious-cli"])
|
|
244
220
|
| if ((.model // "") | tostring | startswith("subconscious/")) then del(.model) else . end
|
|
245
221
|
' "$OPENCODE_CONFIG" >"$tmp"
|
|
246
222
|
mv "$tmp" "$OPENCODE_CONFIG"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared OpenCode provider metadata for subc launch and install paths.
|
|
3
|
+
|
|
4
|
+
# OpenCode groups /models entries by provider.name. Keep this stable and human
|
|
5
|
+
# readable.
|
|
6
|
+
SUBC_OPENCODE_PROVIDER_ID="subconscious"
|
|
7
|
+
SUBC_OPENCODE_PROVIDER_NAME="Subconscious Gateway"
|
|
8
|
+
|
|
9
|
+
subc_opencode_model_display_name() {
|
|
10
|
+
local slug="${1:-}"
|
|
11
|
+
slug="${slug#subconscious/}"
|
|
12
|
+
echo "$slug" | awk -F '[-_]' '
|
|
13
|
+
function format_token(token, lower) {
|
|
14
|
+
lower = tolower(token)
|
|
15
|
+
if (lower ~ /^(gpt|oss|api|gguf|ggml|nomic|vl|it|mlx)$/) return toupper(token)
|
|
16
|
+
if (token ~ /^[0-9]+[bBkKmMgG]$/) return toupper(token)
|
|
17
|
+
if (token ~ /^[qQ][0-9]+$/) return toupper(token)
|
|
18
|
+
if (token ~ /^[0-9]+\.[0-9]+/) return token
|
|
19
|
+
if (token ~ /^[A-Za-z][0-9]+[A-Za-z]$/ || token ~ /^[0-9]+[A-Za-z]$/) return toupper(token)
|
|
20
|
+
return toupper(substr(token, 1, 1)) tolower(substr(token, 2))
|
|
21
|
+
}
|
|
22
|
+
{
|
|
23
|
+
out = ""
|
|
24
|
+
for (i = 1; i <= NF; i++) {
|
|
25
|
+
if ($i == "") continue
|
|
26
|
+
out = out (out == "" ? "" : " ") format_token($i)
|
|
27
|
+
}
|
|
28
|
+
print out
|
|
29
|
+
}
|
|
30
|
+
'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
subc_opencode_build_whitelist_json() {
|
|
34
|
+
local model_id
|
|
35
|
+
WHITELIST_JSON=""
|
|
36
|
+
for model_id in "${SUPPORTED_MODELS[@]}"; do
|
|
37
|
+
if [[ -n "$WHITELIST_JSON" ]]; then
|
|
38
|
+
WHITELIST_JSON="${WHITELIST_JSON},\"${model_id}\""
|
|
39
|
+
else
|
|
40
|
+
WHITELIST_JSON="\"${model_id}\""
|
|
41
|
+
fi
|
|
42
|
+
done
|
|
43
|
+
WHITELIST_JSON="[${WHITELIST_JSON}]"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
subc_opencode_build_models_json() {
|
|
47
|
+
local model_id display_name vision_fields model_json
|
|
48
|
+
MODELS_JSON=""
|
|
49
|
+
for model_id in "${SUPPORTED_MODELS[@]}"; do
|
|
50
|
+
display_name="$(subc_opencode_model_display_name "$model_id")"
|
|
51
|
+
vision_fields=""
|
|
52
|
+
if subc_model_supports_vision "$model_id"; then
|
|
53
|
+
vision_fields=',"attachment":true,"modalities":{"input":["text","image"],"output":["text"]}'
|
|
54
|
+
fi
|
|
55
|
+
model_json="\"${model_id}\":{\"name\":\"${display_name}\",\"tools\":true,\"limit\":{\"context\":${CONTEXT_LIMIT},\"output\":${OUTPUT_LIMIT}}${vision_fields}}"
|
|
56
|
+
if [[ -n "$MODELS_JSON" ]]; then
|
|
57
|
+
MODELS_JSON="${MODELS_JSON},${model_json}"
|
|
58
|
+
else
|
|
59
|
+
MODELS_JSON="$model_json"
|
|
60
|
+
fi
|
|
61
|
+
done
|
|
62
|
+
subc_opencode_build_whitelist_json
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
subc_opencode_build_config_json() {
|
|
66
|
+
local base_url="${1:?base url required}"
|
|
67
|
+
local model="${2:?model required}"
|
|
68
|
+
cat <<EOF
|
|
69
|
+
{"\$schema":"https://opencode.ai/config.json","disabled_providers":["subconscious-cli"],"provider":{"${SUBC_OPENCODE_PROVIDER_ID}":{"npm":"@ai-sdk/openai-compatible","name":"${SUBC_OPENCODE_PROVIDER_NAME}","whitelist":${WHITELIST_JSON},"options":{"baseURL":"${base_url}","apiKey":"{env:SUBCONSCIOUS_API_KEY}","headers":{"x-subconscious-client":"opencode"},"modelsDiscovery":{"enabled":false}},"models":{${MODELS_JSON}}}},"model":"${SUBC_OPENCODE_PROVIDER_ID}/${model}"}
|
|
70
|
+
EOF
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
subc_opencode_build_provider_json() {
|
|
74
|
+
local base_url="${1:?base url required}"
|
|
75
|
+
cat <<EOF
|
|
76
|
+
{
|
|
77
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
78
|
+
"name": "${SUBC_OPENCODE_PROVIDER_NAME}",
|
|
79
|
+
"whitelist": ${WHITELIST_JSON},
|
|
80
|
+
"options": {
|
|
81
|
+
"baseURL": "${base_url}",
|
|
82
|
+
"apiKey": "{env:SUBCONSCIOUS_API_KEY}",
|
|
83
|
+
"headers": {
|
|
84
|
+
"x-subconscious-client": "opencode"
|
|
85
|
+
},
|
|
86
|
+
"modelsDiscovery": {
|
|
87
|
+
"enabled": false
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"models": {${MODELS_JSON}}
|
|
91
|
+
}
|
|
92
|
+
EOF
|
|
93
|
+
}
|
|
@@ -19,6 +19,7 @@ set -euo pipefail
|
|
|
19
19
|
|
|
20
20
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
21
|
source "${SCRIPT_DIR}/../model-capabilities.generated.sh"
|
|
22
|
+
source "${SCRIPT_DIR}/provider.sh"
|
|
22
23
|
|
|
23
24
|
# Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
|
|
24
25
|
SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
|
|
@@ -58,19 +59,7 @@ while IFS= read -r model_id; do
|
|
|
58
59
|
add_supported_model "$model_id"
|
|
59
60
|
done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
for model_id in "${SUPPORTED_MODELS[@]}"; do
|
|
63
|
-
vision_fields=""
|
|
64
|
-
if subc_model_supports_vision "$model_id"; then
|
|
65
|
-
vision_fields=',"attachment":true,"modalities":{"input":["text","image"],"output":["text"]}'
|
|
66
|
-
fi
|
|
67
|
-
model_json="\"${model_id}\":{\"name\":\"${model_id}\",\"tools\":true,\"limit\":{\"context\":${CONTEXT_LIMIT},\"output\":${OUTPUT_LIMIT}}${vision_fields}}"
|
|
68
|
-
if [[ -n "$MODELS_JSON" ]]; then
|
|
69
|
-
MODELS_JSON="${MODELS_JSON},${model_json}"
|
|
70
|
-
else
|
|
71
|
-
MODELS_JSON="$model_json"
|
|
72
|
-
fi
|
|
73
|
-
done
|
|
62
|
+
subc_opencode_build_models_json
|
|
74
63
|
|
|
75
64
|
if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
|
|
76
65
|
echo "error: GATEWAY_URL and API_KEY must be set in ../.env" >&2
|
|
@@ -96,15 +85,11 @@ if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
|
|
|
96
85
|
fi
|
|
97
86
|
|
|
98
87
|
BASE_URL="${GATEWAY_URL%/}/v1"
|
|
99
|
-
RUNTIME_PROVIDER_ID="subconscious-cli"
|
|
100
88
|
|
|
101
89
|
export SUBCONSCIOUS_API_KEY="$API_KEY"
|
|
102
90
|
# Compaction reporting needs the plugin on disk; this launch path writes nothing.
|
|
103
91
|
export SUBCONSCIOUS_GATEWAY_URL="${GATEWAY_URL%/}"
|
|
104
|
-
export OPENCODE_CONFIG_CONTENT
|
|
105
|
-
{"\$schema":"https://opencode.ai/config.json","disabled_providers":["subconscious"],"provider":{"${RUNTIME_PROVIDER_ID}":{"npm":"@ai-sdk/openai-compatible","name":"Subconscious Gateway","options":{"baseURL":"${BASE_URL}","apiKey":"{env:SUBCONSCIOUS_API_KEY}","headers":{"x-subconscious-client":"opencode"}},"models":{${MODELS_JSON}}}},"model":"${RUNTIME_PROVIDER_ID}/${MODEL}"}
|
|
106
|
-
EOF
|
|
107
|
-
)
|
|
92
|
+
export OPENCODE_CONFIG_CONTENT="$(subc_opencode_build_config_json "$BASE_URL" "$MODEL")"
|
|
108
93
|
|
|
109
94
|
# If sourced, just export env and return.
|
|
110
95
|
if [[ "${BASH_SOURCE[0]:-$0}" != "${0}" ]]; then
|