feat: Claude client + Voyage AI embed helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
@@ -3,3 +3,5 @@ NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
CRON_SECRET=<generate with: openssl rand -hex 32>
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
VOYAGE_API_KEY=pa-...
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
|
||||
export const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
export async function embedText(text: string): Promise<number[]> {
|
||||
if (!process.env.VOYAGE_API_KEY) throw new Error('VOYAGE_API_KEY is not set')
|
||||
const res = await fetch('https://api.voyageai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.VOYAGE_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ input: [text], model: 'voyage-3-lite' }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Voyage embed failed: ${res.status}`)
|
||||
const json = await res.json() as { data: Array<{ embedding: number[] }> }
|
||||
return json.data[0].embedding
|
||||
}
|
||||
Generated
+42
-2
@@ -8,6 +8,7 @@
|
||||
"name": "ims-app",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.111.0",
|
||||
"@supabase/ssr": "^0.12.0",
|
||||
"@supabase/supabase-js": "^2.110.2",
|
||||
"next": "^15.5.20",
|
||||
@@ -55,6 +56,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.111.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.111.0.tgz",
|
||||
"integrity": "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"standardwebhooks": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||
@@ -302,7 +324,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -6451,6 +6472,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -8592,6 +8626,12 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
@@ -9437,7 +9477,7 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"generate-qr": "tsx scripts/generate-qr.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.111.0",
|
||||
"@supabase/ssr": "^0.12.0",
|
||||
"@supabase/supabase-js": "^2.110.2",
|
||||
"next": "^15.5.20",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
describe('embedText', () => {
|
||||
beforeEach(() => {
|
||||
process.env.VOYAGE_API_KEY = 'test-key'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('returns 1024-element embedding array', async () => {
|
||||
const mockEmbedding = Array.from({ length: 1024 }, (_, i) => i / 1024)
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [{ embedding: mockEmbedding }] }),
|
||||
}))
|
||||
const { embedText } = await import('@/lib/claude/embed')
|
||||
const result = await embedText('forklift hit racking in zone B')
|
||||
expect(result).toHaveLength(1024)
|
||||
expect(result[0]).toBeCloseTo(0)
|
||||
expect(result[1023]).toBeCloseTo(1023 / 1024)
|
||||
})
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401 }))
|
||||
const { embedText } = await import('@/lib/claude/embed')
|
||||
await expect(embedText('test')).rejects.toThrow('Voyage embed failed: 401')
|
||||
})
|
||||
|
||||
it('throws when VOYAGE_API_KEY is missing', async () => {
|
||||
delete process.env.VOYAGE_API_KEY
|
||||
vi.resetModules()
|
||||
const { embedText } = await import('@/lib/claude/embed')
|
||||
await expect(embedText('test')).rejects.toThrow('VOYAGE_API_KEY')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user