chore: prepare GoodBuddy 0.8.4
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.8.3",
|
"version": "0.8.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.8.3",
|
"version": "0.8.4",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.8.3",
|
"version": "0.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||||
"desktopName": "GoodBuddy",
|
"desktopName": "GoodBuddy",
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ describe('embedding error classification', () => {
|
|||||||
timedOut: true
|
timedOut: true
|
||||||
}).code
|
}).code
|
||||||
).toBe('timeout')
|
).toBe('timeout')
|
||||||
|
const timeout = new Error('Embedding request timed out')
|
||||||
|
timeout.name = 'TimeoutError'
|
||||||
|
expect(
|
||||||
|
classifyEmbeddingError(timeout, { cancelled: true }).code
|
||||||
|
).toBe('timeout')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('never returns provider bodies, credentials, endpoints or nested causes', () => {
|
it('never returns provider bodies, credentials, endpoints or nested causes', () => {
|
||||||
|
|||||||
@@ -105,16 +105,22 @@ function classifyCode(
|
|||||||
const text = errorText(error)
|
const text = errorText(error)
|
||||||
const status = numericStatus(error) ?? statusFromText(text)
|
const status = numericStatus(error) ?? statusFromText(text)
|
||||||
|
|
||||||
|
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||||
|
return 'timeout'
|
||||||
|
}
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return 'cancelled'
|
||||||
|
}
|
||||||
|
if (options.timedOut) {
|
||||||
|
return 'timeout'
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
options.cancelled ||
|
options.cancelled ||
|
||||||
hasAny(text, ['aborterror', 'aborted', 'cancelled', 'canceled'])
|
hasAny(text, ['aborterror', 'aborted', 'cancelled', 'canceled'])
|
||||||
) {
|
) {
|
||||||
return 'cancelled'
|
return 'cancelled'
|
||||||
}
|
}
|
||||||
if (
|
if (hasAny(text, ['timeout', 'timed out', 'etimedout'])) {
|
||||||
options.timedOut ||
|
|
||||||
hasAny(text, ['timeout', 'timed out', 'etimedout'])
|
|
||||||
) {
|
|
||||||
return 'timeout'
|
return 'timeout'
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -181,6 +181,89 @@ describe('EmbeddingIndexCoordinator', () => {
|
|||||||
expect(JSON.stringify(result)).not.toContain('private payload')
|
expect(JSON.stringify(result)).not.toContain('private payload')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('preserves a provider timeout when the diagnostic signal aborts later', async () => {
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
const coordinator = new EmbeddingIndexCoordinator(repository)
|
||||||
|
const controller = new AbortController()
|
||||||
|
let rejectProvider:
|
||||||
|
| ((reason?: unknown) => void)
|
||||||
|
| undefined
|
||||||
|
const diagnostic = coordinator.diagnose(
|
||||||
|
provider(
|
||||||
|
() =>
|
||||||
|
new Promise<number[][]>((_resolve, reject) => {
|
||||||
|
rejectProvider = reject
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ signal: controller.signal }
|
||||||
|
)
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(rejectProvider).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeout = new Error('Embedding request timed out')
|
||||||
|
timeout.name = 'TimeoutError'
|
||||||
|
controller.abort()
|
||||||
|
rejectProvider?.(timeout)
|
||||||
|
|
||||||
|
await expect(diagnostic).resolves.toMatchObject({
|
||||||
|
status: 'unavailable',
|
||||||
|
error: {
|
||||||
|
code: 'timeout'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps diagnostics independent from an active rebuild cancellation', async () => {
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
let rebuildSignal: AbortSignal | undefined
|
||||||
|
let resolveDiagnostic: ((vectors: number[][]) => void) | undefined
|
||||||
|
const embed = vi.fn<EmbeddingIndexProvider['embed']>(
|
||||||
|
(input, signal) => {
|
||||||
|
if (input[0] === 'GoodBuddy 向量模型连接测试') {
|
||||||
|
return new Promise<number[][]>((resolve) => {
|
||||||
|
resolveDiagnostic = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return new Promise<number[][]>((_resolve, reject) => {
|
||||||
|
rebuildSignal = signal
|
||||||
|
signal?.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => reject(signal.reason),
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const sharedProvider = provider(embed)
|
||||||
|
const coordinator = new EmbeddingIndexCoordinator(repository, {
|
||||||
|
createId: () => 'job-concurrent'
|
||||||
|
})
|
||||||
|
coordinator.startRebuild(sharedProvider)
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(rebuildSignal).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
const diagnostic = coordinator.diagnose(sharedProvider)
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(resolveDiagnostic).toBeDefined()
|
||||||
|
})
|
||||||
|
expect(coordinator.cancel('job-concurrent')).toBe(true)
|
||||||
|
resolveDiagnostic?.([[0.25, 0.5, 0.75]])
|
||||||
|
|
||||||
|
await expect(diagnostic).resolves.toMatchObject({
|
||||||
|
status: 'available',
|
||||||
|
dimensions: 3
|
||||||
|
})
|
||||||
|
await expect(coordinator.waitForCompletion()).resolves.toMatchObject({
|
||||||
|
status: 'cancelled'
|
||||||
|
})
|
||||||
|
expect(embed.mock.calls).toContainEqual([
|
||||||
|
['GoodBuddy 向量模型连接测试'],
|
||||||
|
undefined
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('replaces each document atomically and persists completed progress', async () => {
|
it('replaces each document atomically and persists completed progress', async () => {
|
||||||
const repository = new MemoryRepository()
|
const repository = new MemoryRepository()
|
||||||
const coordinator = new EmbeddingIndexCoordinator(repository, {
|
const coordinator = new EmbeddingIndexCoordinator(repository, {
|
||||||
@@ -320,6 +403,42 @@ describe('EmbeddingIndexCoordinator', () => {
|
|||||||
expect(repository.lastJob).toEqual(cancelled)
|
expect(repository.lastJob).toEqual(cancelled)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('preserves a provider timeout when rebuild cancellation arrives later', async () => {
|
||||||
|
const repository = new MemoryRepository()
|
||||||
|
const coordinator = new EmbeddingIndexCoordinator(repository, {
|
||||||
|
createId: () => 'job-timeout'
|
||||||
|
})
|
||||||
|
let rejectProvider:
|
||||||
|
| ((reason?: unknown) => void)
|
||||||
|
| undefined
|
||||||
|
coordinator.startRebuild(
|
||||||
|
provider(
|
||||||
|
() =>
|
||||||
|
new Promise<number[][]>((_resolve, reject) => {
|
||||||
|
rejectProvider = reject
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(rejectProvider).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeout = new Error('Embedding request timed out')
|
||||||
|
timeout.name = 'TimeoutError'
|
||||||
|
expect(coordinator.cancel('job-timeout')).toBe(true)
|
||||||
|
rejectProvider?.(timeout)
|
||||||
|
|
||||||
|
await expect(coordinator.waitForCompletion()).resolves.toMatchObject({
|
||||||
|
status: 'failed',
|
||||||
|
error: {
|
||||||
|
code: 'timeout'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(repository.errors.get('document-1')).toBe(
|
||||||
|
'向量服务响应超时。'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('marks an interrupted persisted job cancelled during initialization', async () => {
|
it('marks an interrupted persisted job cancelled during initialization', async () => {
|
||||||
const repository = new MemoryRepository()
|
const repository = new MemoryRepository()
|
||||||
repository.lastJob = {
|
repository.lastJob = {
|
||||||
|
|||||||
@@ -431,13 +431,15 @@ export class EmbeddingIndexCoordinator {
|
|||||||
await this.repository
|
await this.repository
|
||||||
.discardDocumentReplacement(replacementId)
|
.discardDocumentReplacement(replacementId)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (signal.aborted) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
const safeError =
|
const safeError =
|
||||||
error instanceof EmbeddingOperationError
|
error instanceof EmbeddingOperationError
|
||||||
? error.toSafeError()
|
? error.toSafeError()
|
||||||
: classifyEmbeddingError(error)
|
: classifyEmbeddingError(error, {
|
||||||
|
cancelled: signal.aborted
|
||||||
|
})
|
||||||
|
if (safeError.code === 'cancelled') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
await this.repository.recordDocumentError(
|
await this.repository.recordDocumentError(
|
||||||
document.id,
|
document.id,
|
||||||
provider.provider,
|
provider.provider,
|
||||||
@@ -467,9 +469,13 @@ export class EmbeddingIndexCoordinator {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cancelled =
|
const safeError =
|
||||||
signal.aborted ||
|
error instanceof EmbeddingOperationError
|
||||||
classifyEmbeddingError(error).code === 'cancelled'
|
? error.toSafeError()
|
||||||
|
: classifyEmbeddingError(error, {
|
||||||
|
cancelled: signal.aborted
|
||||||
|
})
|
||||||
|
const cancelled = safeError.code === 'cancelled'
|
||||||
this.updateJob(
|
this.updateJob(
|
||||||
cancelled
|
cancelled
|
||||||
? {
|
? {
|
||||||
@@ -479,10 +485,7 @@ export class EmbeddingIndexCoordinator {
|
|||||||
: {
|
: {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
completedAt: this.now(),
|
completedAt: this.now(),
|
||||||
error:
|
error: safeError
|
||||||
error instanceof EmbeddingOperationError
|
|
||||||
? error.toSafeError()
|
|
||||||
: classifyEmbeddingError(error)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,94 @@ describe('OpenAIEmbeddingClient', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('distinguishes its request timeout from caller cancellation', async () => {
|
||||||
|
const waitForAbort = vi.fn<typeof fetch>(
|
||||||
|
async (_input, init) =>
|
||||||
|
new Promise<Response>((_resolve, reject) => {
|
||||||
|
init?.signal?.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => reject(init.signal?.reason),
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const timedClient = new OpenAIEmbeddingClient({
|
||||||
|
endpoint: 'http://127.0.0.1:11434/v1/embeddings',
|
||||||
|
model: 'nomic-embed-text',
|
||||||
|
timeoutMs: 100,
|
||||||
|
fetch: waitForAbort
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
timedClient.embed(['safe synthetic input'])
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: 'TimeoutError',
|
||||||
|
message: 'Embedding request timed out'
|
||||||
|
})
|
||||||
|
|
||||||
|
const caller = new AbortController()
|
||||||
|
const cancelled = timedClient.embed(
|
||||||
|
['safe synthetic input'],
|
||||||
|
caller.signal
|
||||||
|
)
|
||||||
|
caller.abort(new Error('caller cancelled'))
|
||||||
|
await expect(cancelled).rejects.toMatchObject({
|
||||||
|
name: 'AbortError',
|
||||||
|
message: 'Embedding request was cancelled'
|
||||||
|
})
|
||||||
|
|
||||||
|
let rejectTransport:
|
||||||
|
| ((reason?: unknown) => void)
|
||||||
|
| undefined
|
||||||
|
let transportSignal: AbortSignal | null | undefined
|
||||||
|
const delayedTransport = vi.fn<typeof fetch>(
|
||||||
|
async (_input, init) =>
|
||||||
|
new Promise<Response>((_resolve, reject) => {
|
||||||
|
transportSignal = init?.signal
|
||||||
|
rejectTransport = reject
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const delayedClient = new OpenAIEmbeddingClient({
|
||||||
|
endpoint: 'http://127.0.0.1:11434/v1/embeddings',
|
||||||
|
model: 'nomic-embed-text',
|
||||||
|
timeoutMs: 100,
|
||||||
|
fetch: delayedTransport
|
||||||
|
})
|
||||||
|
const lateCaller = new AbortController()
|
||||||
|
const timeoutThenCancellation = delayedClient.embed(
|
||||||
|
['safe synthetic input'],
|
||||||
|
lateCaller.signal
|
||||||
|
)
|
||||||
|
await vi.waitFor(
|
||||||
|
() => {
|
||||||
|
expect(transportSignal?.aborted).toBe(true)
|
||||||
|
expect(transportSignal?.reason).toMatchObject({
|
||||||
|
name: 'TimeoutError'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ interval: 5, timeout: 500 }
|
||||||
|
)
|
||||||
|
lateCaller.abort()
|
||||||
|
rejectTransport?.(transportSignal?.reason)
|
||||||
|
await expect(timeoutThenCancellation).rejects.toMatchObject({
|
||||||
|
name: 'TimeoutError',
|
||||||
|
message: 'Embedding request timed out'
|
||||||
|
})
|
||||||
|
|
||||||
|
const preCancelled = new AbortController()
|
||||||
|
preCancelled.abort(new Error('caller cancelled before request'))
|
||||||
|
await expect(
|
||||||
|
delayedClient.embed(
|
||||||
|
['safe synthetic input'],
|
||||||
|
preCancelled.signal
|
||||||
|
)
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: 'AbortError',
|
||||||
|
message: 'Embedding request was cancelled'
|
||||||
|
})
|
||||||
|
expect(delayedTransport).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects unsafe endpoints and malformed vectors', async () => {
|
it('rejects unsafe endpoints and malformed vectors', async () => {
|
||||||
expect(
|
expect(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const MAX_URL_LENGTH = 2_048
|
|||||||
const MAX_DIMENSIONS = 8_192
|
const MAX_DIMENSIONS = 8_192
|
||||||
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024
|
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024
|
||||||
const MIN_TIMEOUT_MS = 100
|
const MIN_TIMEOUT_MS = 100
|
||||||
const MAX_TIMEOUT_MS = 120_000
|
const DEFAULT_TIMEOUT_MS = 120_000
|
||||||
|
const MAX_TIMEOUT_MS = DEFAULT_TIMEOUT_MS
|
||||||
|
|
||||||
export interface OpenAIEmbeddingClientOptions {
|
export interface OpenAIEmbeddingClientOptions {
|
||||||
endpoint: string
|
endpoint: string
|
||||||
@@ -64,6 +65,18 @@ function normalizedEndpoint(input: string): string {
|
|||||||
return url.toString()
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function embeddingAbortError(
|
||||||
|
requestSignal: AbortSignal,
|
||||||
|
timeoutError: Error
|
||||||
|
): Error {
|
||||||
|
if (requestSignal.reason === timeoutError) {
|
||||||
|
return timeoutError
|
||||||
|
}
|
||||||
|
const error = new Error('Embedding request was cancelled')
|
||||||
|
error.name = 'AbortError'
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
async function readBoundedJson(response: Response): Promise<unknown> {
|
async function readBoundedJson(response: Response): Promise<unknown> {
|
||||||
const declaredLength = response.headers.get('content-length')
|
const declaredLength = response.headers.get('content-length')
|
||||||
if (
|
if (
|
||||||
@@ -187,7 +200,7 @@ export class OpenAIEmbeddingClient implements EmbeddingProvider {
|
|||||||
MAX_BATCH_SIZE
|
MAX_BATCH_SIZE
|
||||||
)
|
)
|
||||||
this.timeoutMs = boundedInteger(
|
this.timeoutMs = boundedInteger(
|
||||||
options.timeoutMs ?? 15_000,
|
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||||
'timeoutMs',
|
'timeoutMs',
|
||||||
MIN_TIMEOUT_MS,
|
MIN_TIMEOUT_MS,
|
||||||
MAX_TIMEOUT_MS
|
MAX_TIMEOUT_MS
|
||||||
@@ -255,11 +268,19 @@ export class OpenAIEmbeddingClient implements EmbeddingProvider {
|
|||||||
input: readonly string[],
|
input: readonly string[],
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
): Promise<number[][]> {
|
): Promise<number[][]> {
|
||||||
if (signal?.aborted) {
|
const timeoutError = new Error('Embedding request timed out')
|
||||||
throw signal.reason
|
timeoutError.name = 'TimeoutError'
|
||||||
|
const timeoutController = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
timeoutController.abort(timeoutError)
|
||||||
|
}, this.timeoutMs)
|
||||||
|
const requestSignal = signal
|
||||||
|
? AbortSignal.any([signal, timeoutController.signal])
|
||||||
|
: timeoutController.signal
|
||||||
|
if (requestSignal.aborted) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
throw embeddingAbortError(requestSignal, timeoutError)
|
||||||
}
|
}
|
||||||
const timeout = AbortSignal.timeout(this.timeoutMs)
|
|
||||||
const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
'content-type': 'application/json'
|
'content-type': 'application/json'
|
||||||
@@ -267,7 +288,7 @@ export class OpenAIEmbeddingClient implements EmbeddingProvider {
|
|||||||
if (this.apiKey) {
|
if (this.apiKey) {
|
||||||
headers.authorization = `Bearer ${this.apiKey}`
|
headers.authorization = `Bearer ${this.apiKey}`
|
||||||
}
|
}
|
||||||
let response: Response
|
let response: Response | undefined
|
||||||
try {
|
try {
|
||||||
response = await this.transport(this.endpoint, {
|
response = await this.transport(this.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -276,17 +297,25 @@ export class OpenAIEmbeddingClient implements EmbeddingProvider {
|
|||||||
redirect: 'error',
|
redirect: 'error',
|
||||||
signal: requestSignal
|
signal: requestSignal
|
||||||
})
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Embedding request failed with HTTP ${response.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return validateEmbeddings(
|
||||||
|
await readBoundedJson(response),
|
||||||
|
input.length
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestSignal.aborted) {
|
if (requestSignal.aborted) {
|
||||||
const abortError = new Error('Embedding request was cancelled')
|
throw embeddingAbortError(requestSignal, timeoutError)
|
||||||
abortError.name = 'AbortError'
|
}
|
||||||
throw abortError
|
if (response) {
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Embedding request failed', { cause: error })
|
throw new Error('Embedding request failed', { cause: error })
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Embedding request failed with HTTP ${response.status}`)
|
|
||||||
}
|
|
||||||
return validateEmbeddings(await readBoundedJson(response), input.length)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ describe('App', () => {
|
|||||||
it('checks for updates silently on startup and only reports a new version', async () => {
|
it('checks for updates silently on startup and only reports a new version', async () => {
|
||||||
const check = vi.fn(async () => ({
|
const check = vi.fn(async () => ({
|
||||||
updateAvailable: true,
|
updateAvailable: true,
|
||||||
currentVersion: '0.8.3',
|
currentVersion: '0.8.4',
|
||||||
latestVersion: '0.9.0',
|
latestVersion: '0.9.0',
|
||||||
releaseUrl:
|
releaseUrl:
|
||||||
'https://github.com/mesalogo/goodbuddy/releases/tag/v0.9.0',
|
'https://github.com/mesalogo/goodbuddy/releases/tag/v0.9.0',
|
||||||
@@ -595,10 +595,10 @@ describe('App', () => {
|
|||||||
it('does not disturb startup when updates are current or offline', async () => {
|
it('does not disturb startup when updates are current or offline', async () => {
|
||||||
const currentResult = {
|
const currentResult = {
|
||||||
updateAvailable: false,
|
updateAvailable: false,
|
||||||
currentVersion: '0.8.3',
|
currentVersion: '0.8.4',
|
||||||
latestVersion: '0.8.3',
|
latestVersion: '0.8.4',
|
||||||
releaseUrl:
|
releaseUrl:
|
||||||
'https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.3',
|
'https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.4',
|
||||||
target: {
|
target: {
|
||||||
platform: 'windows' as const,
|
platform: 'windows' as const,
|
||||||
arch: 'x64' as const,
|
arch: 'x64' as const,
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ describe('embedding contracts', () => {
|
|||||||
latencyMs: 184,
|
latencyMs: 184,
|
||||||
dimensions: 1_536
|
dimensions: 1_536
|
||||||
})
|
})
|
||||||
|
expect(
|
||||||
|
embeddingDiagnosticResultSchema.safeParse({
|
||||||
|
status: 'available',
|
||||||
|
provider: 'provider',
|
||||||
|
model: 'slow-local-model',
|
||||||
|
checkedAt: 1_700_000_000_000,
|
||||||
|
latencyMs: 120_001,
|
||||||
|
dimensions: 4_096
|
||||||
|
}).success
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
embeddingDiagnosticResultSchema.safeParse({
|
embeddingDiagnosticResultSchema.safeParse({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const embeddingDiagnosticBase = {
|
|||||||
provider: boundedLabelSchema,
|
provider: boundedLabelSchema,
|
||||||
model: boundedLabelSchema,
|
model: boundedLabelSchema,
|
||||||
checkedAt: timestampSchema,
|
checkedAt: timestampSchema,
|
||||||
latencyMs: countSchema.max(120_000)
|
latencyMs: countSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
export const embeddingDiagnosticResultSchema = z.discriminatedUnion('status', [
|
export const embeddingDiagnosticResultSchema = z.discriminatedUnion('status', [
|
||||||
|
|||||||
Reference in New Issue
Block a user