headlesscode 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.
- package/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode index` subcommand — build/refresh the codebase-search index.
|
|
3
|
+
*
|
|
4
|
+
* npx tsx src/cli.ts index --workspace <path> [--model <id>] [--embedding-backend <openrouter|ollama|airunner>]
|
|
5
|
+
*
|
|
6
|
+
* Walks the workspace's real source files (git-aware, .gitignore-respecting),
|
|
7
|
+
* chunks them, embeds ONLY the chunks whose content hash changed since the
|
|
8
|
+
* last index (incremental — the cost-control mechanism), and writes the index
|
|
9
|
+
* into the CENTRAL per-project data store
|
|
10
|
+
* (~/.local/share/headlesscode/projects/<key>/codesearch/index.jsonl — see
|
|
11
|
+
* src/project-store.ts). Worktrees of a repo share that store automatically.
|
|
12
|
+
*
|
|
13
|
+
* Three embedding backends are supported:
|
|
14
|
+
* - openrouter (default) — cloud, `qwen/qwen3-embedding-4b`, costs real
|
|
15
|
+
* money (priced in src/budget/cost.ts);
|
|
16
|
+
* - ollama (opt-in) — local `qwen3-embedding:8b` via
|
|
17
|
+
* HEADLESSCODE_OLLAMA_URL (default http://localhost:11434). Free, no
|
|
18
|
+
* cost tracking; produces 4096-dim vectors, NOT interchangeable with the
|
|
19
|
+
* cloud model's 2560-dim vectors (a backend switch requires a full
|
|
20
|
+
* reindex — the build handles it automatically by treating every stored
|
|
21
|
+
* vector from the other backend as stale);
|
|
22
|
+
* - airunner (opt-in) — local intfloat/e5-large via the AIRunner server's
|
|
23
|
+
* native embedding endpoint (HEADLESSCODE_AIRUNNER_EMBED_URL, default
|
|
24
|
+
* http://localhost:8080). Free, no cost tracking; produces 1024-dim
|
|
25
|
+
* vectors, again NOT interchangeable with the other backends' vectors.
|
|
26
|
+
*
|
|
27
|
+
* Indexing is a SEPARATE, EXPLICIT step (it costs real money and takes real
|
|
28
|
+
* time) — it is never auto-triggered mid-session. Sessions consume whatever
|
|
29
|
+
* index already exists or tell the model to ask a human to build one.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import * as path from "node:path"
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
createEmbedder,
|
|
36
|
+
EMBEDDING_BACKEND_ENV,
|
|
37
|
+
OPENROUTER_EMBEDDING_MODEL_ENV,
|
|
38
|
+
resolveEmbeddingBackend,
|
|
39
|
+
resolveEmbeddingModel,
|
|
40
|
+
} from "./embedder.js"
|
|
41
|
+
import { buildIndex, indexFilePath } from "./index.js"
|
|
42
|
+
import { estimateCost, priceFor } from "../budget/cost.js"
|
|
43
|
+
import { DEFAULT_AIRUNNER_EMBEDDING_MODEL, AIRUNNER_EMBEDDING_MODEL_ENV } from "./airunner-embedder.js"
|
|
44
|
+
import { DEFAULT_OLLAMA_EMBEDDING_MODEL, OLLAMA_EMBEDDING_MODEL_ENV } from "./ollama-embedder.js"
|
|
45
|
+
|
|
46
|
+
const INDEX_USAGE = `headlesscode index — build/refresh the codebase semantic-search index
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
headlesscode index --workspace <path> [--model <id>] [--embedding-backend <openrouter|ollama|airunner>]
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--workspace <path> Workspace root to index (required)
|
|
53
|
+
--model <id> OpenRouter embedding model id (default:
|
|
54
|
+
$OPENROUTER_EMBEDDING_MODEL or ${resolveEmbeddingModel()})
|
|
55
|
+
--embedding-backend <b> Embedding backend: openrouter (default), ollama
|
|
56
|
+
(local, opt-in), or airunner (local AIRunner
|
|
57
|
+
server, opt-in). Default: $HEADLESSCODE_EMBEDDING_BACKEND
|
|
58
|
+
or openrouter
|
|
59
|
+
--help Show this help and exit
|
|
60
|
+
|
|
61
|
+
Environment:
|
|
62
|
+
HEADLESSCODE_EMBEDDING_BACKEND openrouter | ollama | airunner (default openrouter)
|
|
63
|
+
HEADLESSCODE_EMBEDDING_MODEL OpenRouter embedding model override
|
|
64
|
+
HEADLESSCODE_OPENROUTER_EMBEDDING_MODEL OpenRouter embedding model override
|
|
65
|
+
(preferred for clarity)
|
|
66
|
+
HEADLESSCODE_OLLAMA_URL Ollama server URL (default http://localhost:11434)
|
|
67
|
+
HEADLESSCODE_OLLAMA_EMBEDDING_MODEL Ollama embedding model (default ${DEFAULT_OLLAMA_EMBEDDING_MODEL})
|
|
68
|
+
HEADLESSCODE_AIRUNNER_EMBED_URL AIRunner server URL (default http://localhost:8080)
|
|
69
|
+
HEADLESSCODE_AIRUNNER_EMBED_MODEL AIRunner embedding model (default ${DEFAULT_AIRUNNER_EMBEDDING_MODEL})
|
|
70
|
+
|
|
71
|
+
This builds (or incrementally refreshes) the codebase-search index used by the
|
|
72
|
+
codebase_search tool. The index is stored in the CENTRAL per-project data store
|
|
73
|
+
(~/.local/share/headlesscode/projects/<key>/codesearch/index.jsonl) — keyed by
|
|
74
|
+
the repo's git-common-dir, so every worktree of a repo shares one index with
|
|
75
|
+
zero per-directory setup. Chunks whose content hash is unchanged since the last
|
|
76
|
+
index are skipped without re-embedding, so re-running after a small change only
|
|
77
|
+
pays for the changed chunks.
|
|
78
|
+
`
|
|
79
|
+
|
|
80
|
+
interface IndexCliOptions {
|
|
81
|
+
workspace?: string
|
|
82
|
+
model?: string
|
|
83
|
+
embeddingBackend?: string
|
|
84
|
+
help: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseIndexArgs(argv: string[]): { options: IndexCliOptions; error?: string } {
|
|
88
|
+
const options: IndexCliOptions = { help: false }
|
|
89
|
+
for (let i = 0; i < argv.length; i++) {
|
|
90
|
+
const arg = argv[i]
|
|
91
|
+
const eq = arg.indexOf("=")
|
|
92
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
93
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
94
|
+
const next = (): string | undefined => {
|
|
95
|
+
if (inlineValue !== undefined) {
|
|
96
|
+
return inlineValue
|
|
97
|
+
}
|
|
98
|
+
const v = argv[i + 1]
|
|
99
|
+
if (v === undefined || v.startsWith("--")) {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
i++
|
|
103
|
+
return v
|
|
104
|
+
}
|
|
105
|
+
switch (flag) {
|
|
106
|
+
case "--workspace":
|
|
107
|
+
case "--model":
|
|
108
|
+
case "--embedding-backend": {
|
|
109
|
+
const value = next()
|
|
110
|
+
if (value === undefined) {
|
|
111
|
+
return { options, error: `Missing value for ${flag}` }
|
|
112
|
+
}
|
|
113
|
+
if (flag === "--workspace") {
|
|
114
|
+
options.workspace = value
|
|
115
|
+
} else if (flag === "--model") {
|
|
116
|
+
options.model = value
|
|
117
|
+
} else {
|
|
118
|
+
options.embeddingBackend = value
|
|
119
|
+
}
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
case "--help":
|
|
123
|
+
case "-h":
|
|
124
|
+
options.help = true
|
|
125
|
+
break
|
|
126
|
+
default:
|
|
127
|
+
return { options, error: `Unknown argument: ${arg}` }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { options }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function indexMain(argv: string[]): Promise<number> {
|
|
134
|
+
const { options, error } = parseIndexArgs(argv)
|
|
135
|
+
if (error) {
|
|
136
|
+
process.stderr.write(`headlesscode index: ${error}\n\n${INDEX_USAGE}`)
|
|
137
|
+
return 2
|
|
138
|
+
}
|
|
139
|
+
if (options.help) {
|
|
140
|
+
process.stdout.write(INDEX_USAGE)
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
if (!options.workspace) {
|
|
144
|
+
process.stderr.write(`headlesscode index: --workspace <path> is required\n\n${INDEX_USAGE}`)
|
|
145
|
+
return 2
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const workspaceRoot = path.resolve(options.workspace)
|
|
149
|
+
const backend = resolveEmbeddingBackend(process.env, options.embeddingBackend)
|
|
150
|
+
|
|
151
|
+
const embedder = createEmbedder(backend, { model: options.model }, process.env)
|
|
152
|
+
const modelForEnv = options.model?.trim() || process.env[OPENROUTER_EMBEDDING_MODEL_ENV]?.trim()
|
|
153
|
+
|
|
154
|
+
if (backend === "openrouter" && !process.env.HEADLESSCODE_OPENROUTER_API_KEY) {
|
|
155
|
+
process.stderr.write("headlesscode index: HEADLESSCODE_OPENROUTER_API_KEY is not set (required for cloud embedding).\n")
|
|
156
|
+
return 2
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.stdout.write(`Indexing workspace: ${workspaceRoot}\n`)
|
|
160
|
+
process.stdout.write(`Embedding backend: ${backend}\n`)
|
|
161
|
+
process.stdout.write(`Embedding model: ${embedder.model}\n`)
|
|
162
|
+
if (backend === "ollama") {
|
|
163
|
+
const ollamaModel = process.env[OLLAMA_EMBEDDING_MODEL_ENV]?.trim() || DEFAULT_OLLAMA_EMBEDDING_MODEL
|
|
164
|
+
process.stdout.write(` (local, no embedding cost — model: ${ollamaModel})\n`)
|
|
165
|
+
}
|
|
166
|
+
if (backend === "airunner") {
|
|
167
|
+
const airunnerModel = process.env[AIRUNNER_EMBEDDING_MODEL_ENV]?.trim() || DEFAULT_AIRUNNER_EMBEDDING_MODEL
|
|
168
|
+
process.stdout.write(` (local AIRunner server, no embedding cost — model: ${airunnerModel})\n`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await buildIndex(
|
|
173
|
+
embedder,
|
|
174
|
+
workspaceRoot,
|
|
175
|
+
(scanned, total) => {
|
|
176
|
+
process.stdout.write(`\r scanned ${scanned}/${total} files`)
|
|
177
|
+
},
|
|
178
|
+
backend,
|
|
179
|
+
(embedded, total) => {
|
|
180
|
+
process.stdout.write(`\r embedded ${embedded}/${total} chunks`)
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
if (result.filesScanned > 0) {
|
|
184
|
+
process.stdout.write(`\r scanned ${result.filesScanned}/${result.filesScanned} files\n`)
|
|
185
|
+
} else {
|
|
186
|
+
process.stdout.write("\n")
|
|
187
|
+
}
|
|
188
|
+
// The embedding phase used to run fully silent (a large repo looked
|
|
189
|
+
// "stalled" between the scan line and the summary — live report
|
|
190
|
+
// 2026-08-16). Keep the final state of the per-batch progress visible.
|
|
191
|
+
process.stdout.write(` embedded ${result.chunksEmbedded}/${result.chunksEmbedded} chunks\n`)
|
|
192
|
+
|
|
193
|
+
const indexFile = indexFilePath(workspaceRoot)
|
|
194
|
+
const costLine =
|
|
195
|
+
backend === "openrouter"
|
|
196
|
+
? (() => {
|
|
197
|
+
const price = priceFor(result.model)
|
|
198
|
+
const estCostUsd = estimateCost({
|
|
199
|
+
model: result.model,
|
|
200
|
+
inputTokens: result.promptTokens,
|
|
201
|
+
outputTokens: 0,
|
|
202
|
+
})
|
|
203
|
+
return ` estimated cost: $${estCostUsd.toFixed(6)} (${result.model} @ $${price.input}/1M input)\n`
|
|
204
|
+
})()
|
|
205
|
+
: ` estimated cost: $0.00 (local backend — no cloud spend)\n`
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`Index written: ${indexFile}\n` +
|
|
208
|
+
` files scanned: ${result.filesScanned}\n` +
|
|
209
|
+
` chunks embedded: ${result.chunksEmbedded}\n` +
|
|
210
|
+
` chunks skipped (unchanged): ${result.chunksSkipped}\n` +
|
|
211
|
+
` chunks removed: ${result.chunksRemoved}\n` +
|
|
212
|
+
` total chunks: ${result.totalChunks}\n` +
|
|
213
|
+
` embedding tokens: ${result.promptTokens}\n` +
|
|
214
|
+
costLine,
|
|
215
|
+
)
|
|
216
|
+
return 0
|
|
217
|
+
} catch (err) {
|
|
218
|
+
process.stderr.write(
|
|
219
|
+
`headlesscode index: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
220
|
+
)
|
|
221
|
+
return 1
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud (OpenRouter) embedder for the codebase index.
|
|
3
|
+
*
|
|
4
|
+
* SEPARATE from the Phase 3 memory embedder (src/memory/embed.ts,
|
|
5
|
+
* `createLocalEmbedder`) — that one is a deliberately cheap deterministic
|
|
6
|
+
* hash stand-in for handful-of-records recall. This one embeds whole-repo
|
|
7
|
+
* source chunks with a real cloud model and reports real token usage into the
|
|
8
|
+
* budget (src/budget/cost.ts).
|
|
9
|
+
*
|
|
10
|
+
* ─── OpenRouter embeddings — verified findings (2026-08-01) ─────────────────
|
|
11
|
+
*
|
|
12
|
+
* This section is load-bearing: the project owner needs to know exactly what
|
|
13
|
+
* was verified before building on it, because OpenRouter's embeddings story
|
|
14
|
+
* is (as of this date) NOT what a reader might assume.
|
|
15
|
+
*
|
|
16
|
+
* 1. MODEL AVAILABILITY: Embedding models are NOT listed in OpenRouter's
|
|
17
|
+
* public `/api/v1/models` catalog (337 models returned, zero embedding
|
|
18
|
+
* ids, zero embedding-modality entries — verified live against a real
|
|
19
|
+
* API key on 2026-08-01). They are also absent from `/api/v1/models?q=`
|
|
20
|
+
* and from the single-model `/api/v1/models/:id` endpoint (404). The
|
|
21
|
+
* embeddings endpoint itself is LIVE, however: POST /api/v1/embeddings
|
|
22
|
+
* returns proper 4xx errors for bad models, and real embeddings for the
|
|
23
|
+
* few ids the platform currently serves.
|
|
24
|
+
* 2. MODELS THAT ACTUALLY WORK (probed live, real API key, 2026-08-01):
|
|
25
|
+
* - `google/gemini-embedding-001` → 3072 dims, works.
|
|
26
|
+
* - `qwen/qwen3-embedding-4b` → 2560 dims, works (Qwen3-Embedding-4B).
|
|
27
|
+
* - `qwen/qwen3-embedding-8b` → engine overloaded at probe time (the
|
|
28
|
+
* model exists; the endpoint was just busy).
|
|
29
|
+
* Models that FAILED live: `qwen/qwen3-embedding-0.6b` (no endpoints),
|
|
30
|
+
* `openai/text-embedding-3-small`/`-large` (guardrail/data-policy 404),
|
|
31
|
+
* `google/gemini-embedding-002` / `google/text-embedding-004` (do not
|
|
32
|
+
* exist), `mistralai/mistral-embed-2312` / `mistralai/codestral-embed-
|
|
33
|
+
* 2505` (no allowed providers on this account), `google/gemini-
|
|
34
|
+
* embedding-001:free` (no free endpoints).
|
|
35
|
+
* 3. PRICING: OpenRouter's response `usage` object includes `cost` (USD) and
|
|
36
|
+
* `cost_details`, but NOT the model's price-per-token. `qwen/qwen3-
|
|
37
|
+
* embedding-4b` responded with `cost: 0` and `is_byok: true` (a BYOK
|
|
38
|
+
* routed endpoint — the account's own provider key, costed at $0 through
|
|
39
|
+
* OpenRouter). `google/gemini-embedding-001` responded with real cost
|
|
40
|
+
* (~$0.00000015/token → $0.15/1M). Because embedding models are hidden
|
|
41
|
+
* from the catalog, their published OpenRouter list prices could NOT be
|
|
42
|
+
* read from the API on this date, and the doc page is a client-rendered
|
|
43
|
+
* app (no server-rendered content to scrape). We therefore price
|
|
44
|
+
* `qwen/qwen3-embedding-4b` conservatively at the same per-token rate as
|
|
45
|
+
* Google's embedding (input-only, $0.15/1M — see src/budget/cost.ts) and
|
|
46
|
+
* treat OpenRouter's own `usage.cost` as advisory only.
|
|
47
|
+
* 4. BATCHING: confirmed live — N inputs in one request → N embeddings, one
|
|
48
|
+
* HTTP call, `usage.prompt_tokens` summed across all inputs. Embedding an
|
|
49
|
+
* entire repo's changed chunks is therefore ONE request per model-context
|
|
50
|
+
* budget, not one request per chunk.
|
|
51
|
+
* 5. DIMENSIONS: `qwen/qwen3-embedding-4b` = 2560, `google/gemini-
|
|
52
|
+
* embedding-001` = 3072. Vectors are stored as plain JSON number arrays in
|
|
53
|
+
* the index; dimension is whatever the model returns, and cosine similarity
|
|
54
|
+
* is computed over whatever length comes back, so no fixed-dimension
|
|
55
|
+
* constraint needs to be hard-coded.
|
|
56
|
+
*
|
|
57
|
+
* The chosen default model is `qwen/qwen3-embedding-8b` (the 4b→8b switch is
|
|
58
|
+
* documented below; 2560+ dims — a solid quality/size/cost balance for code
|
|
59
|
+
* search), pinned to the DeepInfra provider (see DEFAULT_EMBEDDING_PROVIDER).
|
|
60
|
+
* Both the model and the pin are overridable per machine via the central
|
|
61
|
+
* store's settings.json (src/project-store.ts) and per build via
|
|
62
|
+
* `HEADLESSCODE_EMBEDDING_MODEL` (or the constructor option).
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
import { OpenRouterClient } from "../llm/openrouter.js"
|
|
66
|
+
import { loadCentralSettings, type CentralSettings } from "../project-store.js"
|
|
67
|
+
import { AirunnerEmbedder, resolveAirunnerEmbeddingModel, resolveAirunnerUrl } from "./airunner-embedder.js"
|
|
68
|
+
import {
|
|
69
|
+
DEFAULT_OLLAMA_EMBEDDING_MODEL,
|
|
70
|
+
DEFAULT_OLLAMA_URL,
|
|
71
|
+
OLLAMA_EMBEDDING_MODEL_ENV,
|
|
72
|
+
OLLAMA_URL_ENV,
|
|
73
|
+
OllamaEmbedder,
|
|
74
|
+
resolveOllamaEmbeddingModel,
|
|
75
|
+
resolveOllamaUrl,
|
|
76
|
+
} from "./ollama-embedder.js"
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default embedding model (see header comment for verification notes).
|
|
80
|
+
* The 8b variant (not 4b) — matching airunner's own working setup
|
|
81
|
+
* (projects/uwuchat/server/embedding_provider.py): chosen there for zero data
|
|
82
|
+
* retention + lower cost. The default is overridable per machine via the
|
|
83
|
+
* central store's settings.json (`embedding.model` — see src/project-store.ts)
|
|
84
|
+
* and per build via $HEADLESSCODE_EMBEDDING_MODEL / --model.
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_EMBEDDING_MODEL = "qwen/qwen3-embedding-8b"
|
|
87
|
+
|
|
88
|
+
/** Env var that overrides the embedding model. */
|
|
89
|
+
export const EMBEDDING_MODEL_ENV = "HEADLESSCODE_EMBEDDING_MODEL"
|
|
90
|
+
|
|
91
|
+
/** Embedding backends (the codebase index build + query can pick between). */
|
|
92
|
+
export const EMBEDDING_BACKENDS = ["openrouter", "ollama", "airunner"] as const
|
|
93
|
+
|
|
94
|
+
/** Default backend — OpenRouter, matching the pre-existing behavior. */
|
|
95
|
+
export const DEFAULT_EMBEDDING_BACKEND = "openrouter"
|
|
96
|
+
|
|
97
|
+
/** Env var that selects the backend (`openrouter` | `ollama` | `airunner`). */
|
|
98
|
+
export const EMBEDDING_BACKEND_ENV = "HEADLESSCODE_EMBEDDING_BACKEND"
|
|
99
|
+
|
|
100
|
+
/** Env var that overrides the OpenRouter embedding model. */
|
|
101
|
+
export const OPENROUTER_EMBEDDING_MODEL_ENV = "HEADLESSCODE_OPENROUTER_EMBEDDING_MODEL"
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Default OpenRouter provider pin for embedding requests, matching airunner's
|
|
105
|
+
* working setup (projects/uwuchat/server/embedding_provider.py): DeepInfra,
|
|
106
|
+
* zero data retention, no fallbacks. Verified live 2026-08-04 against
|
|
107
|
+
* OpenRouter's `/api/v1/models/<id>/endpoints` that the provider slug casing
|
|
108
|
+
* is exactly `"DeepInfra"` (capital D/I). Overridable per machine via the
|
|
109
|
+
* central store's settings.json (`embedding.provider` /
|
|
110
|
+
* `embedding.allowFallbacks` — see src/project-store.ts).
|
|
111
|
+
*/
|
|
112
|
+
export const DEFAULT_EMBEDDING_PROVIDER = "DeepInfra"
|
|
113
|
+
export const DEFAULT_EMBEDDING_ALLOW_FALLBACKS = false
|
|
114
|
+
|
|
115
|
+
/** The backend an index was built with (recorded per entry + in metadata). */
|
|
116
|
+
export type EmbeddingBackend = (typeof EMBEDDING_BACKENDS)[number]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the embedding backend: explicit override → env → default. The
|
|
120
|
+
* default is deliberately `openrouter` (opt-in for Ollama), because hosted/
|
|
121
|
+
* headless workers have no local GPU — see plans/local-embeddings-ollama.md.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveEmbeddingBackend(env: NodeJS.ProcessEnv = process.env, override?: string): EmbeddingBackend {
|
|
124
|
+
const raw = (override?.trim() || env[EMBEDDING_BACKEND_ENV]?.trim() || DEFAULT_EMBEDDING_BACKEND).toLowerCase()
|
|
125
|
+
if (raw === "ollama" || raw === "openrouter" || raw === "airunner") {
|
|
126
|
+
return raw
|
|
127
|
+
}
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid embedding backend "${raw}" (from ${EMBEDDING_BACKEND_ENV} or --embedding-backend): expected "openrouter", "ollama", or "airunner"`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Embedding batch size (chunks per request). Kept modest for retry granularity. */
|
|
134
|
+
export const EMBED_BATCH_SIZE = 128
|
|
135
|
+
|
|
136
|
+
/** Max tokens per chunk before we refuse to embed it (4 chars/token heuristic). */
|
|
137
|
+
export const MAX_CHUNK_CHARS = 8_000
|
|
138
|
+
|
|
139
|
+
/** How many consecutive 429/overloaded responses before giving up. */
|
|
140
|
+
const MAX_RETRIES = 3
|
|
141
|
+
const INITIAL_RETRY_DELAY_MS = 500
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the embedding model: constructor option → env → central settings →
|
|
145
|
+
* built-in default. The central settings default (src/project-store.ts's
|
|
146
|
+
* settings.json `embedding.model`) is the per-machine override knob so the
|
|
147
|
+
* 8b default is not hardcoded in application code with no way to change it.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveEmbeddingModel(env: NodeJS.ProcessEnv = process.env, override?: string): string {
|
|
150
|
+
const settings = loadCentralSettings()
|
|
151
|
+
return (
|
|
152
|
+
override?.trim() ||
|
|
153
|
+
env[EMBEDDING_MODEL_ENV]?.trim() ||
|
|
154
|
+
settings.embedding?.model?.trim() ||
|
|
155
|
+
DEFAULT_EMBEDDING_MODEL
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the OpenRouter provider pin for embedding requests: central settings
|
|
161
|
+
* → built-in default. Returns undefined only when settings explicitly disable
|
|
162
|
+
* pinning (allowFallbacks without a provider is treated as no pin).
|
|
163
|
+
*/
|
|
164
|
+
export function resolveEmbeddingProvider(settings: CentralSettings = loadCentralSettings()): {
|
|
165
|
+
order?: string[]
|
|
166
|
+
allowFallbacks?: boolean
|
|
167
|
+
} | undefined {
|
|
168
|
+
const provider = settings.embedding?.provider?.trim()
|
|
169
|
+
if (!provider) {
|
|
170
|
+
return { order: [DEFAULT_EMBEDDING_PROVIDER], allowFallbacks: DEFAULT_EMBEDDING_ALLOW_FALLBACKS }
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
order: [provider],
|
|
174
|
+
...(typeof settings.embedding?.allowFallbacks === "boolean"
|
|
175
|
+
? { allowFallbacks: settings.embedding.allowFallbacks }
|
|
176
|
+
: { allowFallbacks: DEFAULT_EMBEDDING_ALLOW_FALLBACKS }),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The embedder interface the index build + tool handler depend on. */
|
|
181
|
+
export interface Embedder {
|
|
182
|
+
/** Model id used for embedding (for pricing/usage accounting). */
|
|
183
|
+
readonly model: string
|
|
184
|
+
/**
|
|
185
|
+
* Embed a batch of text chunks. Returns one vector per input, in order,
|
|
186
|
+
* plus the real token usage (for budget accounting).
|
|
187
|
+
*/
|
|
188
|
+
embedBatch(texts: string[]): Promise<{ embeddings: number[][]; promptTokens: number; totalTokens: number }>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Result of one embedder batch, including model + cost info for accounting. */
|
|
192
|
+
export interface EmbedResult {
|
|
193
|
+
embeddings: number[][]
|
|
194
|
+
model: string
|
|
195
|
+
promptTokens: number
|
|
196
|
+
totalTokens: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Name of the backend field on the index metadata block (shared with index.ts/types.ts). */
|
|
200
|
+
export const METADATA_BACKEND_KEY = "backend" as const
|
|
201
|
+
|
|
202
|
+
/** Name of the model field on the index metadata block (shared with index.ts/types.ts). */
|
|
203
|
+
export const METADATA_MODEL_KEY = "model" as const
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create the embedder for a backend, defaulting to `openrouter`. The
|
|
207
|
+
* OpenRouter model resolves from the constructor option → env → default; the
|
|
208
|
+
* Ollama model/URL from their own env vars (or constructor options).
|
|
209
|
+
*/
|
|
210
|
+
export function createEmbedder(
|
|
211
|
+
backend?: EmbeddingBackend,
|
|
212
|
+
options: {
|
|
213
|
+
/** OpenRouter embedding model override (--model / HEADLESSCODE_OPENROUTER_EMBEDDING_MODEL). */
|
|
214
|
+
model?: string
|
|
215
|
+
/** Ollama embedding model override (HEADLESSCODE_OLLAMA_EMBEDDING_MODEL). */
|
|
216
|
+
ollamaModel?: string
|
|
217
|
+
/** Ollama server URL override (HEADLESSCODE_OLLAMA_URL). */
|
|
218
|
+
ollamaUrl?: string
|
|
219
|
+
/** AIRunner embedding model override (HEADLESSCODE_AIRUNNER_EMBED_MODEL). */
|
|
220
|
+
airunnerModel?: string
|
|
221
|
+
/** AIRunner server URL override (HEADLESSCODE_AIRUNNER_EMBED_URL). */
|
|
222
|
+
airunnerUrl?: string
|
|
223
|
+
} = {},
|
|
224
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
225
|
+
): Embedder {
|
|
226
|
+
const resolved = resolveEmbeddingBackend(env, backend)
|
|
227
|
+
if (resolved === "ollama") {
|
|
228
|
+
return new OllamaEmbedder(options.ollamaModel ?? resolveOllamaEmbeddingModel(env), options.ollamaUrl ?? resolveOllamaUrl(env))
|
|
229
|
+
}
|
|
230
|
+
if (resolved === "airunner") {
|
|
231
|
+
return new AirunnerEmbedder(
|
|
232
|
+
options.airunnerModel ?? resolveAirunnerEmbeddingModel(env),
|
|
233
|
+
options.airunnerUrl ?? resolveAirunnerUrl(env),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
return new OpenRouterEmbedder(
|
|
237
|
+
options.model ?? env[OPENROUTER_EMBEDDING_MODEL_ENV] ?? resolveEmbeddingModel(env),
|
|
238
|
+
undefined,
|
|
239
|
+
resolveEmbeddingProvider(),
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* OpenRouter-backed embedder. Batches chunks (one HTTP call per batch), retries
|
|
245
|
+
* transient failures with backoff, and returns real usage for budget feeding.
|
|
246
|
+
*/
|
|
247
|
+
export class OpenRouterEmbedder implements Embedder {
|
|
248
|
+
private readonly client: OpenRouterClient
|
|
249
|
+
private readonly provider: { order?: string[]; allowFallbacks?: boolean } | undefined
|
|
250
|
+
|
|
251
|
+
constructor(
|
|
252
|
+
readonly model: string = resolveEmbeddingModel(),
|
|
253
|
+
client?: OpenRouterClient,
|
|
254
|
+
provider: { order?: string[]; allowFallbacks?: boolean } | undefined = resolveEmbeddingProvider(),
|
|
255
|
+
) {
|
|
256
|
+
this.client = client ?? new OpenRouterClient()
|
|
257
|
+
this.provider = provider
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async embedBatch(texts: string[]): Promise<EmbedResult> {
|
|
261
|
+
if (texts.length === 0) {
|
|
262
|
+
return { embeddings: [], model: this.model, promptTokens: 0, totalTokens: 0 }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Defensive: embedding providers reject empty strings outright (live
|
|
266
|
+
// crash 2026-08-16 — OpenRouter HTTP 400 "too_small"). buildIndex
|
|
267
|
+
// filters these before calling, but any other caller must get a clear
|
|
268
|
+
// diagnostic here, not a cryptic 400 from upstream.
|
|
269
|
+
const emptyIndex = texts.findIndex((t) => t.trim() === "")
|
|
270
|
+
if (emptyIndex !== -1) {
|
|
271
|
+
throw new Error(`embedder: cannot embed empty/whitespace-only input at index ${emptyIndex} — providers reject it`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const embeddings: number[][] = []
|
|
275
|
+
let promptTokens = 0
|
|
276
|
+
let totalTokens = 0
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < texts.length; i += EMBED_BATCH_SIZE) {
|
|
279
|
+
const batch = texts.slice(i, i + EMBED_BATCH_SIZE)
|
|
280
|
+
let attempt = 0
|
|
281
|
+
let lastError: unknown
|
|
282
|
+
while (attempt < MAX_RETRIES) {
|
|
283
|
+
try {
|
|
284
|
+
const result = await this.client.embed(batch, this.model, { provider: this.provider })
|
|
285
|
+
embeddings.push(...result.embeddings)
|
|
286
|
+
promptTokens += result.promptTokens
|
|
287
|
+
totalTokens += result.totalTokens
|
|
288
|
+
lastError = undefined
|
|
289
|
+
break
|
|
290
|
+
} catch (err) {
|
|
291
|
+
lastError = err
|
|
292
|
+
const status =
|
|
293
|
+
err instanceof Error && "status" in err ? (err as { status?: number }).status : undefined
|
|
294
|
+
const isTransient = status === 429 || status === 502 || status === 503
|
|
295
|
+
if (!isTransient || attempt >= MAX_RETRIES - 1) {
|
|
296
|
+
throw err
|
|
297
|
+
}
|
|
298
|
+
await new Promise((r) => setTimeout(r, INITIAL_RETRY_DELAY_MS * 2 ** attempt))
|
|
299
|
+
attempt++
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (lastError) {
|
|
303
|
+
throw lastError
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { embeddings, model: this.model, promptTokens, totalTokens }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Compute cosine similarity between two vectors. Handles different lengths by
|
|
313
|
+
* treating missing entries as 0 (index entries are all the same model's
|
|
314
|
+
* dimension in practice, but a mixed old/new index should not crash).
|
|
315
|
+
*/
|
|
316
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
317
|
+
let dot = 0
|
|
318
|
+
let normA = 0
|
|
319
|
+
let normB = 0
|
|
320
|
+
const len = Math.max(a.length, b.length)
|
|
321
|
+
for (let i = 0; i < len; i++) {
|
|
322
|
+
const av = a[i] ?? 0
|
|
323
|
+
const bv = b[i] ?? 0
|
|
324
|
+
dot += av * bv
|
|
325
|
+
normA += av * av
|
|
326
|
+
normB += bv * bv
|
|
327
|
+
}
|
|
328
|
+
if (normA === 0 || normB === 0) {
|
|
329
|
+
return 0
|
|
330
|
+
}
|
|
331
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB))
|
|
332
|
+
}
|