@gotcos/glasses-server 6.38.0 → 6.39.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/.env.example +21 -1
- package/CHANGELOG.md +55 -0
- package/README.md +14 -1
- package/package.json +2 -2
- package/server/lib/archive.ts +52 -5
- package/server/lib/attached-provider-adapter.ts +2 -48
- package/server/lib/banned-permission-args.ts +45 -0
- package/server/lib/claude-bridge.ts +2 -1
- package/server/lib/codex-bridge.ts +49 -4
- package/server/lib/codex-engine-sessions.ts +8 -2
- package/server/lib/codex-extra-args.ts +222 -0
- package/server/lib/even-hub-speaker-role.ts +116 -0
- package/server/lib/fork-thread.ts +2 -2
- package/server/lib/health-static-probes.ts +23 -1
- package/server/lib/model-router.ts +14 -0
- package/server/lib/ollama-bridge.ts +274 -0
- package/server/lib/ollama-catalog.ts +161 -0
- package/server/lib/ollama-run-ledger.ts +178 -0
- package/server/lib/query-job-runtime.ts +8 -2
- package/server/lib/query-job-types.ts +2 -1
- package/server/routes/health.ts +25 -0
- package/server/routes/openai-compat.ts +2 -0
- package/server/routes/transcribe-stream.ts +25 -0
- package/server/scripts/repair-archive-duplicates.ts +181 -0
- package/shared/model-preference.ts +25 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { findBannedPermissionArg } from './banned-permission-args.js'
|
|
2
|
+
|
|
3
|
+
export const CODEX_EXTRA_ARGS_MAX_TOKENS = 32
|
|
4
|
+
export const CODEX_EXTRA_ARGS_MAX_CHARS = 2_000
|
|
5
|
+
|
|
6
|
+
const BOOLEAN_FLAGS = new Set(['--oss'])
|
|
7
|
+
const VALUE_FLAGS = new Set(['-c', '--config', '--model', '-m', '--local-provider'])
|
|
8
|
+
const ALLOWED_FLAGS = new Set(['--oss', '--local-provider', '--model', '-m', '-c', '--config'])
|
|
9
|
+
const ALLOWED_CONFIG_KEYS = new Set([
|
|
10
|
+
'model',
|
|
11
|
+
'model_provider',
|
|
12
|
+
'oss_provider',
|
|
13
|
+
'model_reasoning_effort',
|
|
14
|
+
'service_tier',
|
|
15
|
+
])
|
|
16
|
+
const LOCAL_PROVIDER_VALUES = new Set(['ollama', 'lmstudio'])
|
|
17
|
+
|
|
18
|
+
export class CodexExtraArgsError extends Error {
|
|
19
|
+
flag: string
|
|
20
|
+
constructor(flag: string) {
|
|
21
|
+
super(`Invalid COS_CODEX_EXTRA_ARGS (${flag}). Edit ~/.cos-glasses/.env and restart the glasses server.`)
|
|
22
|
+
this.name = 'CodexExtraArgsError'
|
|
23
|
+
this.flag = flag
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extraArgsUserMessage(flag: string): string {
|
|
28
|
+
return `Invalid COS_CODEX_EXTRA_ARGS (${flag}). Edit ~/.cos-glasses/.env and restart the glasses server.`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extraArgsErrorMessage(flag: string): string {
|
|
32
|
+
return extraArgsUserMessage(flag)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unwrapWholeValue(raw: string): string {
|
|
36
|
+
if (raw.length >= 2) {
|
|
37
|
+
const first = raw[0]
|
|
38
|
+
const last = raw[raw.length - 1]
|
|
39
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
40
|
+
return raw.slice(1, -1).trim()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return raw
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function splitUnquotedWhitespace(raw: string): string[] {
|
|
47
|
+
const tokens: string[] = []
|
|
48
|
+
let current = ''
|
|
49
|
+
let quote: '"' | "'" | null = null
|
|
50
|
+
for (const ch of raw) {
|
|
51
|
+
if (quote) {
|
|
52
|
+
current += ch
|
|
53
|
+
if (ch === quote) quote = null
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"' || ch === "'") {
|
|
57
|
+
quote = ch
|
|
58
|
+
current += ch
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (/\s/.test(ch)) {
|
|
62
|
+
if (current) {
|
|
63
|
+
tokens.push(current)
|
|
64
|
+
current = ''
|
|
65
|
+
}
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
current += ch
|
|
69
|
+
}
|
|
70
|
+
if (quote) throw new CodexExtraArgsError('quotes')
|
|
71
|
+
if (current) tokens.push(current)
|
|
72
|
+
return tokens
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseCodexExtraArgs(raw: string | undefined): string[] {
|
|
76
|
+
const trimmed = (raw ?? '').trim()
|
|
77
|
+
if (!trimmed) return []
|
|
78
|
+
if (trimmed.length > CODEX_EXTRA_ARGS_MAX_CHARS) throw new CodexExtraArgsError('length')
|
|
79
|
+
const unwrapped = unwrapWholeValue(trimmed)
|
|
80
|
+
if (!unwrapped) return []
|
|
81
|
+
const tokens = splitUnquotedWhitespace(unwrapped)
|
|
82
|
+
if (tokens.length > CODEX_EXTRA_ARGS_MAX_TOKENS) throw new CodexExtraArgsError('length')
|
|
83
|
+
return tokens
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripWrappingQuotes(value: string): string {
|
|
87
|
+
if (value.length >= 2) {
|
|
88
|
+
const first = value[0]
|
|
89
|
+
const last = value[value.length - 1]
|
|
90
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
91
|
+
return value.slice(1, -1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fusedValueFlag(token: string): string | null {
|
|
98
|
+
for (const flag of VALUE_FLAGS) {
|
|
99
|
+
if (token.startsWith(`${flag}=`)) return flag
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Walked =
|
|
105
|
+
| { kind: 'boolean'; flag: string }
|
|
106
|
+
| { kind: 'value'; flag: string; value: string }
|
|
107
|
+
|
|
108
|
+
function walkCodexExtraArgs(args: string[]): Walked[] {
|
|
109
|
+
const walked: Walked[] = []
|
|
110
|
+
let i = 0
|
|
111
|
+
while (i < args.length) {
|
|
112
|
+
const token = args[i]
|
|
113
|
+
if (BOOLEAN_FLAGS.has(token)) {
|
|
114
|
+
walked.push({ kind: 'boolean', flag: token })
|
|
115
|
+
i += 1
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
const fused = fusedValueFlag(token)
|
|
119
|
+
if (fused) {
|
|
120
|
+
const value = token.slice(fused.length + 1)
|
|
121
|
+
if (!value) throw new CodexExtraArgsError(fused)
|
|
122
|
+
if (value.startsWith('-')) throw new CodexExtraArgsError(fused)
|
|
123
|
+
walked.push({ kind: 'value', flag: fused, value })
|
|
124
|
+
i += 1
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
if (VALUE_FLAGS.has(token)) {
|
|
128
|
+
const value = args[i + 1]
|
|
129
|
+
if (value === undefined) throw new CodexExtraArgsError(token)
|
|
130
|
+
if (value.startsWith('-')) throw new CodexExtraArgsError(token)
|
|
131
|
+
walked.push({ kind: 'value', flag: token, value })
|
|
132
|
+
i += 2
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
throw new CodexExtraArgsError(token)
|
|
136
|
+
}
|
|
137
|
+
return walked
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function configPayloads(walked: Walked[]): { key: string; value: string }[] {
|
|
141
|
+
const payloads: { key: string; value: string }[] = []
|
|
142
|
+
for (const step of walked) {
|
|
143
|
+
if (step.kind !== 'value' || (step.flag !== '-c' && step.flag !== '--config')) continue
|
|
144
|
+
const eq = step.value.indexOf('=')
|
|
145
|
+
if (eq <= 0) throw new CodexExtraArgsError(`-c ${stripWrappingQuotes(step.value)}`)
|
|
146
|
+
const key = stripWrappingQuotes(step.value.slice(0, eq))
|
|
147
|
+
const value = step.value.slice(eq + 1)
|
|
148
|
+
payloads.push({ key, value })
|
|
149
|
+
}
|
|
150
|
+
return payloads
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function extraIncludesFlag(args: string[], flags: string[]): boolean {
|
|
154
|
+
for (const token of args) {
|
|
155
|
+
for (const flag of flags) {
|
|
156
|
+
if (token === flag || token.startsWith(`${flag}=`)) return true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function extraIncludesConfigKey(args: string[], key: string): boolean {
|
|
163
|
+
const walked = walkCodexExtraArgs(args)
|
|
164
|
+
return configPayloads(walked).some(payload => payload.key === key)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function assertSafeCodexExtraArgs(args: string[]): void {
|
|
168
|
+
if (args.length === 0) return
|
|
169
|
+
const walked = walkCodexExtraArgs(args)
|
|
170
|
+
for (const step of walked) {
|
|
171
|
+
if (!ALLOWED_FLAGS.has(step.flag)) throw new CodexExtraArgsError(step.flag)
|
|
172
|
+
}
|
|
173
|
+
const hasOss = walked.some(step => step.kind === 'boolean' && step.flag === '--oss')
|
|
174
|
+
const hasLocalProvider = extraIncludesFlag(args, ['--local-provider'])
|
|
175
|
+
const payloads = configPayloads(walked)
|
|
176
|
+
const hasOssProvider = payloads.some(payload => payload.key === 'oss_provider')
|
|
177
|
+
const hasModelProvider = payloads.some(payload => payload.key === 'model_provider')
|
|
178
|
+
|
|
179
|
+
if (hasOss && hasModelProvider) throw new CodexExtraArgsError('-c model_provider')
|
|
180
|
+
if (hasOss && !hasLocalProvider && !hasOssProvider) throw new CodexExtraArgsError('--oss')
|
|
181
|
+
if ((hasLocalProvider || hasOssProvider) && !hasOss) {
|
|
182
|
+
throw new CodexExtraArgsError(hasLocalProvider ? '--local-provider' : '-c oss_provider')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const step of walked) {
|
|
186
|
+
if (step.kind !== 'value') continue
|
|
187
|
+
if (step.flag === '--local-provider') {
|
|
188
|
+
if (!LOCAL_PROVIDER_VALUES.has(stripWrappingQuotes(step.value))) {
|
|
189
|
+
throw new CodexExtraArgsError('--local-provider')
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const payload of payloads) {
|
|
194
|
+
if (!ALLOWED_CONFIG_KEYS.has(payload.key)) throw new CodexExtraArgsError(`-c ${payload.key}`)
|
|
195
|
+
if (payload.key === 'oss_provider' && !LOCAL_PROVIDER_VALUES.has(stripWrappingQuotes(payload.value))) {
|
|
196
|
+
throw new CodexExtraArgsError('-c oss_provider')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const banned = findBannedPermissionArg(args)
|
|
201
|
+
if (banned !== null) throw new CodexExtraArgsError(banned)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function skipsCosModel(extra: string[]): boolean {
|
|
205
|
+
return extraIncludesFlag(extra, ['--model', '-m']) || extraIncludesConfigKey(extra, 'model')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function skipsCosServiceTier(extra: string[]): boolean {
|
|
209
|
+
return extraIncludesFlag(extra, ['--oss', '--local-provider'])
|
|
210
|
+
|| extraIncludesConfigKey(extra, 'model_provider')
|
|
211
|
+
|| extraIncludesConfigKey(extra, 'service_tier')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function skipsCosReasoningEffort(extra: string[]): boolean {
|
|
215
|
+
return extraIncludesConfigKey(extra, 'model_reasoning_effort')
|
|
216
|
+
|| extraIncludesFlag(extra, ['--oss', '--local-provider'])
|
|
217
|
+
|| extraIncludesConfigKey(extra, 'model_provider')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function codexEngineFingerprint(extra: string[]): string {
|
|
221
|
+
return extra.join('\0')
|
|
222
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Even Hub 0.0.14 wearer-vs-other histogram, carried on a meeting chunk.
|
|
2
|
+
// Identity is a suggestion. This module parses and logs. It does not name
|
|
3
|
+
// people and does not change identifyChunkSpeaker.
|
|
4
|
+
|
|
5
|
+
export type EvenSpeakerRole = 'self' | 'other' | 'unknown'
|
|
6
|
+
export type EvenSpeakerRoleMajority = EvenSpeakerRole | 'tie'
|
|
7
|
+
|
|
8
|
+
export interface EvenSpeakerRoleHistogram {
|
|
9
|
+
schema: 1
|
|
10
|
+
frames: number
|
|
11
|
+
self: number
|
|
12
|
+
other: number
|
|
13
|
+
unknown: number
|
|
14
|
+
majority: EvenSpeakerRoleMajority
|
|
15
|
+
directionPresent: number
|
|
16
|
+
directionLast: number | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type EvenSpeakerRoleMode = 'off' | 'log' | 'apply'
|
|
20
|
+
|
|
21
|
+
export function evenSpeakerRoleMode(): EvenSpeakerRoleMode {
|
|
22
|
+
const raw = (process.env.COS_EVEN_SPEAKER_ROLE ?? 'log').trim().toLowerCase()
|
|
23
|
+
if (raw === 'off' || raw === '0' || raw === 'false') return 'off'
|
|
24
|
+
if (raw === 'apply') return 'apply'
|
|
25
|
+
return 'log'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let applyNotImplementedWarned = false
|
|
29
|
+
|
|
30
|
+
/** Gate A is not in this slice. apply must not silently change labels. */
|
|
31
|
+
export function warnEvenSpeakerRoleApplyNotImplemented(): void {
|
|
32
|
+
if (evenSpeakerRoleMode() !== 'apply' || applyNotImplementedWarned) return
|
|
33
|
+
applyNotImplementedWarned = true
|
|
34
|
+
console.warn('[even-role] COS_EVEN_SPEAKER_ROLE=apply is not implemented; logging only')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function majorityOf(self: number, other: number, unknown: number, frames: number): EvenSpeakerRoleMajority {
|
|
38
|
+
if (frames <= 0) return 'unknown'
|
|
39
|
+
if (self > other && self > unknown) return 'self'
|
|
40
|
+
if (other > self && other > unknown) return 'other'
|
|
41
|
+
if (unknown > self && unknown > other) return 'unknown'
|
|
42
|
+
return 'tie'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asNonNegInt(raw: unknown): number | null {
|
|
46
|
+
const n = typeof raw === 'number' ? raw : typeof raw === 'string' && raw !== '' ? Number(raw) : NaN
|
|
47
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) return null
|
|
48
|
+
return n
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Compact query `eh=self,other,unknown,frames,directionPresent,directionLast`. */
|
|
52
|
+
export function parseEvenHubSpeakerRoleQuery(raw: unknown): EvenSpeakerRoleHistogram | undefined {
|
|
53
|
+
if (typeof raw !== 'string' || raw.length === 0) return undefined
|
|
54
|
+
const parts = raw.split(',')
|
|
55
|
+
if (parts.length < 4 || parts.length > 6) return undefined
|
|
56
|
+
const self = asNonNegInt(parts[0])
|
|
57
|
+
const other = asNonNegInt(parts[1])
|
|
58
|
+
const unknown = asNonNegInt(parts[2])
|
|
59
|
+
const frames = asNonNegInt(parts[3])
|
|
60
|
+
if (self == null || other == null || unknown == null || frames == null) return undefined
|
|
61
|
+
if (self + other + unknown !== frames) return undefined
|
|
62
|
+
const directionPresent = parts.length >= 5 ? asNonNegInt(parts[4]) : 0
|
|
63
|
+
if (directionPresent == null) return undefined
|
|
64
|
+
let directionLast: number | null = null
|
|
65
|
+
if (parts.length === 6 && parts[5] !== '') {
|
|
66
|
+
const last = Number(parts[5])
|
|
67
|
+
if (!Number.isFinite(last)) return undefined
|
|
68
|
+
directionLast = last
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
schema: 1,
|
|
72
|
+
frames,
|
|
73
|
+
self,
|
|
74
|
+
other,
|
|
75
|
+
unknown,
|
|
76
|
+
majority: majorityOf(self, other, unknown, frames),
|
|
77
|
+
directionPresent,
|
|
78
|
+
directionLast,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseEvenHubSpeakerRoleBody(raw: unknown): EvenSpeakerRoleHistogram | undefined {
|
|
83
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
84
|
+
const o = raw as Record<string, unknown>
|
|
85
|
+
const self = asNonNegInt(o.self)
|
|
86
|
+
const other = asNonNegInt(o.other)
|
|
87
|
+
const unknown = asNonNegInt(o.unknown)
|
|
88
|
+
const frames = asNonNegInt(o.frames)
|
|
89
|
+
if (self == null || other == null || unknown == null || frames == null) return undefined
|
|
90
|
+
if (self + other + unknown !== frames) return undefined
|
|
91
|
+
const directionPresent = o.directionPresent == null ? 0 : asNonNegInt(o.directionPresent)
|
|
92
|
+
if (directionPresent == null) return undefined
|
|
93
|
+
const directionLast = o.directionLast == null || o.directionLast === ''
|
|
94
|
+
? null
|
|
95
|
+
: (typeof o.directionLast === 'number' && Number.isFinite(o.directionLast) ? o.directionLast : null)
|
|
96
|
+
return {
|
|
97
|
+
schema: 1,
|
|
98
|
+
frames,
|
|
99
|
+
self,
|
|
100
|
+
other,
|
|
101
|
+
unknown,
|
|
102
|
+
majority: majorityOf(self, other, unknown, frames),
|
|
103
|
+
directionPresent,
|
|
104
|
+
directionLast,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatEvenRoleAgreement(opts: {
|
|
109
|
+
chunkIndex: number
|
|
110
|
+
even: EvenSpeakerRoleHistogram
|
|
111
|
+
amp: string
|
|
112
|
+
emb: string
|
|
113
|
+
similarity: number
|
|
114
|
+
}): string {
|
|
115
|
+
return `[even-role] chunk=${opts.chunkIndex} even=${opts.even.majority} amp=${opts.amp} emb=${opts.emb} sim=${opts.similarity.toFixed(2)} frames=${opts.even.frames}`
|
|
116
|
+
}
|
|
@@ -78,7 +78,6 @@ import {
|
|
|
78
78
|
buildAttachedEnv,
|
|
79
79
|
classifyStderr,
|
|
80
80
|
extractNativeIdsFromLine,
|
|
81
|
-
findBannedPermissionArg,
|
|
82
81
|
isAttachedPermissionPolicy,
|
|
83
82
|
resolveProviderBinary,
|
|
84
83
|
type AttachedChildProcess,
|
|
@@ -86,6 +85,7 @@ import {
|
|
|
86
85
|
type AttachedStderrClass,
|
|
87
86
|
type BinaryResolution,
|
|
88
87
|
} from './attached-provider-adapter.js'
|
|
88
|
+
import { findBannedPermissionArg } from './banned-permission-args.js'
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Providers that can be forked. A strict subset of the attached set.
|
|
@@ -261,7 +261,7 @@ export function buildCodexForkArgs(nativeThreadId: string, cwd: string): string[
|
|
|
261
261
|
/**
|
|
262
262
|
* Build the argv, and REFUSE to hand back one carrying a banned permission flag.
|
|
263
263
|
*
|
|
264
|
-
* The predicate is IMPORTED from
|
|
264
|
+
* The predicate is IMPORTED from banned-permission-args, not re-listed here. Two copies of a
|
|
265
265
|
* ban list in two modules is precisely the drift `native-thread-id.ts` was created
|
|
266
266
|
* to end: the occupancy detector and the binding store each had their own idea of
|
|
267
267
|
* what a thread id was, and a truncated id walked through the gap. A fork spawns a
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
isCursorProviderReady,
|
|
7
7
|
resolveAgentBinary,
|
|
8
8
|
} from './cursor-model-catalog.js'
|
|
9
|
+
import {
|
|
10
|
+
getOllamaCatalog,
|
|
11
|
+
getOllamaCatalogSnapshot,
|
|
12
|
+
isOllamaProviderReady,
|
|
13
|
+
} from './ollama-catalog.js'
|
|
9
14
|
|
|
10
15
|
const DEFAULT_CACHE_TTL_MS = 30_000
|
|
11
16
|
const PROBE_TIMEOUT_MS = 5_000
|
|
@@ -15,9 +20,11 @@ export interface HealthStaticProbeSnapshot {
|
|
|
15
20
|
claude: string
|
|
16
21
|
codex: string
|
|
17
22
|
cursor: string
|
|
23
|
+
ollama: string
|
|
18
24
|
claudeAvailable: boolean
|
|
19
25
|
codexAvailable: boolean
|
|
20
26
|
cursorAvailable: boolean
|
|
27
|
+
ollamaAvailable: boolean
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
interface CachedProbe<T> {
|
|
@@ -153,21 +160,36 @@ async function probeCursor(): Promise<{ value: string; available: boolean }> {
|
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
async function probeOllama(): Promise<{ value: string; available: boolean }> {
|
|
164
|
+
try {
|
|
165
|
+
const catalog = await getOllamaCatalog()
|
|
166
|
+
const available = isOllamaProviderReady()
|
|
167
|
+
if (available) return { value: catalog.model || 'available', available: true }
|
|
168
|
+
return { value: catalog.error || 'unavailable', available: false }
|
|
169
|
+
} catch {
|
|
170
|
+
const snapshot = getOllamaCatalogSnapshot()
|
|
171
|
+
return { value: snapshot.error || 'error', available: false }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
async function loadStaticHealthProbes(): Promise<HealthStaticProbeSnapshot> {
|
|
157
|
-
const [python, claude, codex, cursor] = await Promise.all([
|
|
176
|
+
const [python, claude, codex, cursor, ollama] = await Promise.all([
|
|
158
177
|
probePython(),
|
|
159
178
|
probeClaude(),
|
|
160
179
|
probeCodex(),
|
|
161
180
|
probeCursor(),
|
|
181
|
+
probeOllama(),
|
|
162
182
|
])
|
|
163
183
|
return {
|
|
164
184
|
python,
|
|
165
185
|
claude: claude.value,
|
|
166
186
|
codex: codex.value,
|
|
167
187
|
cursor: cursor.value,
|
|
188
|
+
ollama: ollama.value,
|
|
168
189
|
claudeAvailable: claude.available,
|
|
169
190
|
codexAvailable: codex.available,
|
|
170
191
|
cursorAvailable: cursor.available,
|
|
192
|
+
ollamaAvailable: ollama.available,
|
|
171
193
|
}
|
|
172
194
|
}
|
|
173
195
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { callClaudeStreaming, type CallOptions, type StreamCallbacks } from './claude-bridge.js'
|
|
2
2
|
import { callCodexStreaming } from './codex-bridge.js'
|
|
3
3
|
import { callCursorStreaming } from './cursor-bridge.js'
|
|
4
|
+
import { callOllamaStreaming } from './ollama-bridge.js'
|
|
4
5
|
import {
|
|
5
6
|
getOrCreateSession,
|
|
6
7
|
getSessionModel,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
isCodexModel,
|
|
14
15
|
isClaudeModel,
|
|
15
16
|
isCursorModel,
|
|
17
|
+
isOllamaModel,
|
|
16
18
|
normalizeModelPreference,
|
|
17
19
|
} from '../../shared/model-preference.js'
|
|
18
20
|
import {
|
|
@@ -20,6 +22,10 @@ import {
|
|
|
20
22
|
isCursorProviderReady,
|
|
21
23
|
resolveCursorModelOption,
|
|
22
24
|
} from './cursor-model-catalog.js'
|
|
25
|
+
import {
|
|
26
|
+
getOllamaCatalog,
|
|
27
|
+
isOllamaProviderReady,
|
|
28
|
+
} from './ollama-catalog.js'
|
|
23
29
|
import type { ModelImageInput } from './model-image-input.js'
|
|
24
30
|
|
|
25
31
|
// Bridges return as soon as their subprocess is spawned, while completion is
|
|
@@ -112,6 +118,14 @@ export async function callModelStreaming(
|
|
|
112
118
|
}
|
|
113
119
|
return await callCursorStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
|
|
114
120
|
}
|
|
121
|
+
if (isOllamaModel(resolvedModel)) {
|
|
122
|
+
await getOllamaCatalog()
|
|
123
|
+
if (!isOllamaProviderReady()) {
|
|
124
|
+
await lockedCallbacks.onError('ollama-bridge: Ollama is not running. Start ollama serve on this Mac.')
|
|
125
|
+
return sid
|
|
126
|
+
}
|
|
127
|
+
return await callOllamaStreaming(query, sid, lockedCallbacks, images, reference, globalMsgNum, options)
|
|
128
|
+
}
|
|
115
129
|
if (isCodexModel(resolvedModel)) {
|
|
116
130
|
return await callCodexStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
|
|
117
131
|
}
|