@takara-ai/miru-code 1.7.8 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -11
- package/docs/kiro-transform-research-synopsis.md +79 -0
- package/docs/miru-aws-transform-concept.md +74 -0
- package/docs/miru-aws-transform-kiro-power-spec.md +98 -0
- package/docs/self-hosted-sagemaker-identity-center.md +109 -0
- package/docs/self-hosted-sagemaker.md +107 -24
- package/package.json +7 -1
- package/src/benchmark/grep.ts +9 -0
- package/src/benchmark/locate-compare.ts +33 -12
- package/src/benchmark/rg-literal.ts +130 -24
- package/src/embeddings/sagemaker.ts +166 -3
- package/src/literal.ts +1 -1
- package/src/mcp/index-cache.ts +52 -7
- package/src/mcp/server.ts +77 -16
- package/src/miru-index.ts +4 -1
- package/src/ranking/boosting.ts +6 -9
- package/src/ranking/config.ts +35 -0
- package/src/ranking/location.ts +5 -4
- package/src/ranking/penalties.ts +4 -3
- package/src/ranking/weighting.ts +4 -1
- package/src/search.ts +13 -8
- package/src/setup.ts +1 -0
- package/src/snippet.ts +343 -6
- package/src/utils.ts +8 -2
- package/src/version.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takara-ai/miru-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,10 +54,15 @@
|
|
|
54
54
|
"lint": "biome check .",
|
|
55
55
|
"lint:fix": "biome check --write .",
|
|
56
56
|
"prepare": "bun run scripts/install-git-hooks.ts",
|
|
57
|
+
"build:kiro-plugin": "bun run scripts/build-kiro-plugin.ts",
|
|
57
58
|
"vendor-grammars": "bun run scripts/vendor-grammars.ts",
|
|
58
59
|
"prepublishOnly": "bun run vendor-grammars && bun run typecheck && bun test",
|
|
59
60
|
"benchmark:ab": "bun run scripts/search-ab-local.ts",
|
|
60
61
|
"benchmark:ab-multi": "bun run scripts/search-ab-multi.ts",
|
|
62
|
+
"benchmark:codesearchnet-compression": "bun run scripts/benchmark-codesearchnet-compression.ts",
|
|
63
|
+
"benchmark:codesearchnet-alpha": "bun run scripts/calibrate-codesearchnet-alpha.ts",
|
|
64
|
+
"benchmark:ranking-sweep": "bun run scripts/sweep-ranking-configs.ts",
|
|
65
|
+
"benchmark:ranking-factorial": "bun run scripts/sweep-ranking-factorial.ts",
|
|
61
66
|
"benchmark:recall": "bun run scripts/benchmark-recall-tokens.ts",
|
|
62
67
|
"benchmark:vs-grep": "bun run scripts/benchmark-miru-vs-grep.ts",
|
|
63
68
|
"benchmark:workflow": "bun run scripts/benchmark-workflow-tokens.ts",
|
|
@@ -71,6 +76,7 @@
|
|
|
71
76
|
},
|
|
72
77
|
"dependencies": {
|
|
73
78
|
"@aws-sdk/client-sagemaker-runtime": "^3.1090.0",
|
|
79
|
+
"@aws-sdk/credential-provider-node": "^3.972.70",
|
|
74
80
|
"ignore": "^7.0.5",
|
|
75
81
|
"web-tree-sitter": "^0.26.9",
|
|
76
82
|
"zod": "^4.4.3"
|
package/src/benchmark/grep.ts
CHANGED
|
@@ -26,6 +26,12 @@ export const RG_EXCLUDE_ARGS = [
|
|
|
26
26
|
"!tokenizer",
|
|
27
27
|
"-g",
|
|
28
28
|
"!tokenizer/**",
|
|
29
|
+
"-g",
|
|
30
|
+
"!*.map",
|
|
31
|
+
"-g",
|
|
32
|
+
"!*.min.js",
|
|
33
|
+
"-g",
|
|
34
|
+
"!*.min.css",
|
|
29
35
|
] as const;
|
|
30
36
|
|
|
31
37
|
export class BenchmarkSearchTimeoutError extends Error {
|
|
@@ -236,6 +242,9 @@ async function grepRankedMatches(repoRoot: string, pattern: string, topK: number
|
|
|
236
242
|
"--exclude-dir=node_modules",
|
|
237
243
|
"--exclude-dir=.git",
|
|
238
244
|
"--exclude-dir=tokenizer",
|
|
245
|
+
"--exclude=*.map",
|
|
246
|
+
"--exclude=*.min.js",
|
|
247
|
+
"--exclude=*.min.css",
|
|
239
248
|
pattern,
|
|
240
249
|
repoRoot,
|
|
241
250
|
]);
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Compare Miru `locate` MCP response tokens vs the costlier native Grep path.
|
|
3
3
|
*
|
|
4
4
|
* Miru side counts the agent-facing text body (`formatLiteralLocateText`), not
|
|
5
|
-
* the intermediate JSON payload.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* real workflow cost — not an apples-to-apples contest against minimal ripgrep.
|
|
5
|
+
* the intermediate JSON payload. The grep baseline requests only the same
|
|
6
|
+
* information: counts for `count`, line locations for `locations`, and context
|
|
7
|
+
* for `lines`. It also mirrors locate's include/exclude scope and variants.
|
|
9
8
|
*/
|
|
10
9
|
|
|
10
|
+
import { join } from "node:path";
|
|
11
11
|
import {
|
|
12
12
|
DEFAULT_LITERAL_MODE,
|
|
13
|
+
filterChunksByGlob,
|
|
13
14
|
formatLiteralLocate,
|
|
14
15
|
type LiteralLocateOptions,
|
|
15
16
|
type LiteralLocateResult,
|
|
@@ -18,7 +19,7 @@ import { formatLiteralLocateText } from "../mcp/format-text.ts";
|
|
|
18
19
|
import type { MiruIndex } from "../miru-index.ts";
|
|
19
20
|
import { countTokens } from "../token-count.ts";
|
|
20
21
|
import { GREP_CONTEXT } from "./grep.ts";
|
|
21
|
-
import { rgLiteralOutput } from "./rg-literal.ts";
|
|
22
|
+
import { rgLiteralOutput, selectComparableLiteralSearchTool } from "./rg-literal.ts";
|
|
22
23
|
import { agentBenchmarkFromTokens } from "./summary.ts";
|
|
23
24
|
import type { AgentBenchmarkSummary } from "./types.ts";
|
|
24
25
|
|
|
@@ -28,6 +29,8 @@ export interface LocateBenchmarkComparison {
|
|
|
28
29
|
result: LiteralLocateResult;
|
|
29
30
|
payload: Record<string, unknown>;
|
|
30
31
|
benchmark: AgentBenchmarkSummary;
|
|
32
|
+
/** Baseline match stats; compare with `result.n`/`result.files` for exact-literal parity. */
|
|
33
|
+
grep: { n: number; files: number };
|
|
31
34
|
latency_ms: {
|
|
32
35
|
miru: number;
|
|
33
36
|
grep: number;
|
|
@@ -40,6 +43,16 @@ export async function benchmarkLocateComparison(options: {
|
|
|
40
43
|
index: MiruIndex;
|
|
41
44
|
locate?: LiteralLocateOptions;
|
|
42
45
|
}): Promise<LocateBenchmarkComparison> {
|
|
46
|
+
if (options.locate?.limit != null) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Cannot benchmark a limited locate response against native grep: rg/grep limits are per file, while locate.limit is global. Omit limit to keep token and recall comparisons valid.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!selectComparableLiteralSearchTool()) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"A comparable literal benchmark requires rg or compatible grep; findstr is not equivalent for scoped locate output.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
43
56
|
const locateOpts: LiteralLocateOptions = { mode: DEFAULT_LITERAL_MODE, ...options.locate };
|
|
44
57
|
|
|
45
58
|
const miruStart = performance.now();
|
|
@@ -48,22 +61,30 @@ export async function benchmarkLocateComparison(options: {
|
|
|
48
61
|
const payload = formatLiteralLocate(result);
|
|
49
62
|
const miruTok = countTokens(formatLiteralLocateText(payload));
|
|
50
63
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
// Grep every variant `match_variants`/an array `literal` actually matched — one
|
|
65
|
+
// pattern is not equivalent recall. Keep contextual lines only for `lines` mode.
|
|
66
|
+
const context =
|
|
67
|
+
locateOpts.mode === "lines" ? Math.max(GREP_CONTEXT, locateOpts.context_lines ?? 0) : 0;
|
|
68
|
+
const indexedPaths = [
|
|
69
|
+
...new Set(
|
|
70
|
+
filterChunksByGlob(options.index.chunks, locateOpts.include, locateOpts.exclude).map(
|
|
71
|
+
(chunk) => join(options.repoPath, chunk.file_path),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
];
|
|
57
75
|
const grep = await rgLiteralOutput(options.repoPath, result.literals ?? [options.literal], {
|
|
58
|
-
context
|
|
76
|
+
context,
|
|
59
77
|
maxCount: 0,
|
|
60
78
|
ignoreCase: locateOpts.ignore_case,
|
|
79
|
+
countOnly: locateOpts.mode === "count",
|
|
80
|
+
paths: indexedPaths,
|
|
61
81
|
});
|
|
62
82
|
|
|
63
83
|
return {
|
|
64
84
|
result,
|
|
65
85
|
payload,
|
|
66
86
|
benchmark: agentBenchmarkFromTokens(miruTok, grep.tokens, result.n > 0),
|
|
87
|
+
grep: { n: grep.n, files: grep.files },
|
|
67
88
|
latency_ms: { miru: miruMs, grep: grep.latency_ms },
|
|
68
89
|
};
|
|
69
90
|
}
|
|
@@ -4,6 +4,9 @@ import { relative } from "node:path";
|
|
|
4
4
|
import { countTokens } from "../token-count.ts";
|
|
5
5
|
import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool, spawnBenchmarkSearch } from "./grep.ts";
|
|
6
6
|
|
|
7
|
+
/** Keep each native-search invocation well below platform command-line limits. */
|
|
8
|
+
const MAX_LITERAL_PATH_ARGUMENT_CHARS = 4_096;
|
|
9
|
+
|
|
7
10
|
export interface RgLiteralOutput {
|
|
8
11
|
text: string;
|
|
9
12
|
tokens: number;
|
|
@@ -12,6 +15,52 @@ export interface RgLiteralOutput {
|
|
|
12
15
|
files: number;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
export interface RgLiteralOptions {
|
|
19
|
+
context?: number;
|
|
20
|
+
maxCount?: number;
|
|
21
|
+
ignoreCase?: boolean;
|
|
22
|
+
/** Return per-file match counts rather than matching lines. */
|
|
23
|
+
countOnly?: boolean;
|
|
24
|
+
/** Gitignore-style globs included by the comparable locate call. */
|
|
25
|
+
include?: string[];
|
|
26
|
+
/** Gitignore-style globs excluded by the comparable locate call. */
|
|
27
|
+
exclude?: string[];
|
|
28
|
+
/** Absolute or repo-relative files in the same corpus as the compared tool. */
|
|
29
|
+
paths?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `findstr` cannot reproduce the scoped, contextual, count-mode literal baseline.
|
|
34
|
+
* Do not silently substitute it and report the result as an equivalent grep comparison.
|
|
35
|
+
*/
|
|
36
|
+
export function selectComparableLiteralSearchTool(
|
|
37
|
+
options?: Parameters<typeof selectBenchmarkSearchTool>[0],
|
|
38
|
+
): "rg" | "grep" | null {
|
|
39
|
+
const tool = selectBenchmarkSearchTool(options);
|
|
40
|
+
return tool === "rg" || tool === "grep" ? tool : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Split an indexed corpus into safe native-command argument batches. */
|
|
44
|
+
export function batchLiteralPaths(paths: readonly string[]): string[][] {
|
|
45
|
+
const batches: string[][] = [];
|
|
46
|
+
let batch: string[] = [];
|
|
47
|
+
let chars = 0;
|
|
48
|
+
for (const path of paths) {
|
|
49
|
+
const nextChars = chars + path.length + 1;
|
|
50
|
+
if (batch.length > 0 && nextChars > MAX_LITERAL_PATH_ARGUMENT_CHARS) {
|
|
51
|
+
batches.push(batch);
|
|
52
|
+
batch = [];
|
|
53
|
+
chars = 0;
|
|
54
|
+
}
|
|
55
|
+
batch.push(path);
|
|
56
|
+
chars += path.length + 1;
|
|
57
|
+
}
|
|
58
|
+
if (batch.length > 0) {
|
|
59
|
+
batches.push(batch);
|
|
60
|
+
}
|
|
61
|
+
return batches;
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
/** Count match lines (path:line:…) and unique files from `rg -n` output. */
|
|
16
65
|
export function parseRgLiteralStats(text: string, repoRoot: string): { n: number; files: number } {
|
|
17
66
|
const files = new Set<string>();
|
|
@@ -59,29 +108,69 @@ function parsePathLinePrefix(line: string): { path: string; line: number } | nul
|
|
|
59
108
|
return null;
|
|
60
109
|
}
|
|
61
110
|
|
|
111
|
+
function parseCountStats(text: string, repoRoot: string): { n: number; files: number } {
|
|
112
|
+
let n = 0;
|
|
113
|
+
let files = 0;
|
|
114
|
+
for (const line of text.split("\n")) {
|
|
115
|
+
const colon = line.lastIndexOf(":");
|
|
116
|
+
if (colon < 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const count = Number(line.slice(colon + 1));
|
|
120
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Ensure this is a path under the requested root, rather than an arbitrary line.
|
|
124
|
+
const path = line.slice(0, colon);
|
|
125
|
+
if (relative(repoRoot, path).startsWith("..")) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
n += count;
|
|
129
|
+
files += 1;
|
|
130
|
+
}
|
|
131
|
+
return { n, files };
|
|
132
|
+
}
|
|
133
|
+
|
|
62
134
|
export async function rgLiteralOutput(
|
|
63
135
|
repoRoot: string,
|
|
64
136
|
literal: string | readonly string[],
|
|
65
|
-
options: {
|
|
66
|
-
context?: number;
|
|
67
|
-
maxCount?: number;
|
|
68
|
-
ignoreCase?: boolean;
|
|
69
|
-
} = {},
|
|
137
|
+
options: RgLiteralOptions = {},
|
|
70
138
|
): Promise<RgLiteralOutput> {
|
|
71
139
|
const literals = Array.isArray(literal) ? literal : [literal as string];
|
|
72
140
|
const context = options.context ?? 0;
|
|
73
141
|
const maxCount = options.maxCount ?? 20;
|
|
74
|
-
const tool =
|
|
142
|
+
const tool = selectComparableLiteralSearchTool();
|
|
75
143
|
if (!tool) {
|
|
76
|
-
throw new Error(
|
|
144
|
+
throw new Error(
|
|
145
|
+
"A comparable literal benchmark requires rg or compatible grep; findstr cannot preserve locate scope, context, and count semantics.",
|
|
146
|
+
);
|
|
77
147
|
}
|
|
78
148
|
const start = performance.now();
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
149
|
+
const pathBatches = options.paths?.length ? batchLiteralPaths(options.paths) : [undefined];
|
|
150
|
+
const output: string[] = [];
|
|
151
|
+
for (const paths of pathBatches) {
|
|
152
|
+
output.push(
|
|
153
|
+
await spawnBenchmarkSearch(
|
|
154
|
+
buildLiteralArgs(
|
|
155
|
+
tool,
|
|
156
|
+
repoRoot,
|
|
157
|
+
literals,
|
|
158
|
+
context,
|
|
159
|
+
maxCount,
|
|
160
|
+
!!options.ignoreCase,
|
|
161
|
+
options.countOnly,
|
|
162
|
+
options.include,
|
|
163
|
+
options.exclude,
|
|
164
|
+
paths,
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const text = output.filter(Boolean).join("\n");
|
|
83
170
|
const latency_ms = performance.now() - start;
|
|
84
|
-
const stats =
|
|
171
|
+
const stats = options.countOnly
|
|
172
|
+
? parseCountStats(text, repoRoot)
|
|
173
|
+
: parseRgLiteralStats(text, repoRoot);
|
|
85
174
|
return { text, tokens: countTokens(text), latency_ms, ...stats };
|
|
86
175
|
}
|
|
87
176
|
|
|
@@ -90,15 +179,28 @@ export async function rgLiteralOutput(
|
|
|
90
179
|
* to get the same recall as `literals`/`match_variants` (one pattern isn't equivalent).
|
|
91
180
|
*/
|
|
92
181
|
function buildLiteralArgs(
|
|
93
|
-
tool: "rg" | "grep"
|
|
182
|
+
tool: "rg" | "grep",
|
|
94
183
|
repoRoot: string,
|
|
95
184
|
literals: readonly string[],
|
|
96
185
|
context: number,
|
|
97
186
|
maxCount: number,
|
|
98
187
|
ignoreCase: boolean,
|
|
188
|
+
countOnly = false,
|
|
189
|
+
include: string[] = [],
|
|
190
|
+
exclude: string[] = [],
|
|
191
|
+
paths?: string[],
|
|
99
192
|
): string[] {
|
|
100
193
|
if (tool === "rg") {
|
|
101
194
|
const args = ["rg", "-F", "-n", "--no-heading", ...RG_EXCLUDE_ARGS];
|
|
195
|
+
if (countOnly) {
|
|
196
|
+
args.splice(2, 2, "--count");
|
|
197
|
+
}
|
|
198
|
+
for (const glob of include) {
|
|
199
|
+
args.push("-g", glob);
|
|
200
|
+
}
|
|
201
|
+
for (const glob of exclude) {
|
|
202
|
+
args.push("-g", `!${glob}`);
|
|
203
|
+
}
|
|
102
204
|
if (ignoreCase) {
|
|
103
205
|
args.push("-i");
|
|
104
206
|
}
|
|
@@ -111,7 +213,7 @@ function buildLiteralArgs(
|
|
|
111
213
|
for (const l of literals) {
|
|
112
214
|
args.push("-e", l);
|
|
113
215
|
}
|
|
114
|
-
args.push(repoRoot);
|
|
216
|
+
args.push(...(paths?.length ? paths : [repoRoot]));
|
|
115
217
|
return args;
|
|
116
218
|
}
|
|
117
219
|
if (tool === "grep") {
|
|
@@ -124,7 +226,19 @@ function buildLiteralArgs(
|
|
|
124
226
|
"--exclude-dir=node_modules",
|
|
125
227
|
"--exclude-dir=.git",
|
|
126
228
|
"--exclude-dir=tokenizer",
|
|
229
|
+
"--exclude=*.map",
|
|
230
|
+
"--exclude=*.min.js",
|
|
231
|
+
"--exclude=*.min.css",
|
|
127
232
|
];
|
|
233
|
+
for (const glob of include) {
|
|
234
|
+
args.push(`--include=${glob}`);
|
|
235
|
+
}
|
|
236
|
+
for (const glob of exclude) {
|
|
237
|
+
args.push(`--exclude=${glob}`);
|
|
238
|
+
}
|
|
239
|
+
if (countOnly) {
|
|
240
|
+
args.push("-c");
|
|
241
|
+
}
|
|
128
242
|
if (ignoreCase) {
|
|
129
243
|
args.push("-i");
|
|
130
244
|
}
|
|
@@ -137,16 +251,8 @@ function buildLiteralArgs(
|
|
|
137
251
|
for (const l of literals) {
|
|
138
252
|
args.push("-e", l);
|
|
139
253
|
}
|
|
140
|
-
args.push(repoRoot);
|
|
254
|
+
args.push(...(paths?.length ? paths : [repoRoot]));
|
|
141
255
|
return args;
|
|
142
256
|
}
|
|
143
|
-
|
|
144
|
-
for (const l of literals) {
|
|
145
|
-
args.push(`/C:${l}`);
|
|
146
|
-
}
|
|
147
|
-
args.push("*");
|
|
148
|
-
if (ignoreCase) {
|
|
149
|
-
args.splice(4, 0, "/I");
|
|
150
|
-
}
|
|
151
|
-
return args;
|
|
257
|
+
throw new Error(`Unsupported comparable literal benchmark tool: ${tool}`);
|
|
152
258
|
}
|
|
@@ -1,22 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import type { InvokeEndpointCommandOutput } from "@aws-sdk/client-sagemaker-runtime";
|
|
2
5
|
import type { EmbeddingClient, EmbeddingPayload, EmbeddingResponse } from "./openai.ts";
|
|
3
6
|
|
|
4
7
|
type SageMakerRuntimeModule = typeof import("@aws-sdk/client-sagemaker-runtime");
|
|
8
|
+
type AwsCredentialProviderModule = typeof import("@aws-sdk/credential-provider-node");
|
|
9
|
+
type StaticAwsCredentials = {
|
|
10
|
+
accessKeyId: string;
|
|
11
|
+
secretAccessKey: string;
|
|
12
|
+
sessionToken?: string;
|
|
13
|
+
};
|
|
5
14
|
|
|
6
15
|
/** Lazily loaded so Takara-only MCP/CLI processes never pay the AWS SDK startup cost. */
|
|
7
16
|
let sageMakerRuntimePromise: Promise<SageMakerRuntimeModule> | null = null;
|
|
17
|
+
let awsCredentialProviderPromise: Promise<AwsCredentialProviderModule> | null = null;
|
|
8
18
|
|
|
9
19
|
function loadSageMakerRuntime(): Promise<SageMakerRuntimeModule> {
|
|
10
20
|
sageMakerRuntimePromise ??= import("@aws-sdk/client-sagemaker-runtime");
|
|
11
21
|
return sageMakerRuntimePromise;
|
|
12
22
|
}
|
|
13
23
|
|
|
24
|
+
function loadAwsCredentialProvider(): Promise<AwsCredentialProviderModule> {
|
|
25
|
+
awsCredentialProviderPromise ??= import("@aws-sdk/credential-provider-node");
|
|
26
|
+
return awsCredentialProviderPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/** arn:aws:sagemaker:<region>:<account-id>:endpoint/<endpoint-name> (also covers aws-cn/aws-us-gov). */
|
|
15
30
|
const ENDPOINT_ARN_PATTERN = /^arn:aws[a-z0-9-]*:sagemaker:([a-z0-9-]+):(\d{12}):endpoint\/(.+)$/;
|
|
16
31
|
|
|
17
32
|
export interface SageMakerEmbeddingConfig {
|
|
18
33
|
endpointName: string;
|
|
19
34
|
region: string;
|
|
35
|
+
profile?: string;
|
|
20
36
|
normalize: boolean;
|
|
21
37
|
truncate: boolean;
|
|
22
38
|
truncationDirection: "Left" | "Right";
|
|
@@ -54,6 +70,103 @@ function resolveTruncationDirection(): "Left" | "Right" {
|
|
|
54
70
|
return process.env.MIRU_SAGEMAKER_TRUNCATION_DIRECTION?.trim() === "Left" ? "Left" : "Right";
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
function parseIniProfiles(contents: string): Map<string, Record<string, string>> {
|
|
74
|
+
const profiles = new Map<string, Record<string, string>>();
|
|
75
|
+
let section: Record<string, string> | undefined;
|
|
76
|
+
|
|
77
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
83
|
+
section = {};
|
|
84
|
+
profiles.set(trimmed.slice(1, -1).trim().toLowerCase(), section);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!section) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const separator = trimmed.indexOf("=");
|
|
91
|
+
if (separator <= 0) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const key = trimmed.slice(0, separator).trim().toLowerCase();
|
|
95
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
96
|
+
section[key] = value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return profiles;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* AWS CLI accepts credential-key casing that the AWS SDK's INI parser does not.
|
|
104
|
+
* Keep this fallback deliberately limited to static credentials; SSO, role, and
|
|
105
|
+
* credential_process profiles remain handled by the SDK's normal provider chain.
|
|
106
|
+
*/
|
|
107
|
+
export function parseCaseInsensitiveStaticCredentials(
|
|
108
|
+
configContents: string,
|
|
109
|
+
credentialsContents: string,
|
|
110
|
+
profileName: string,
|
|
111
|
+
): StaticAwsCredentials | null {
|
|
112
|
+
const profile = profileName.trim().toLowerCase();
|
|
113
|
+
if (!profile) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const merged: Record<string, string> = {};
|
|
118
|
+
for (const [sectionName, values] of parseIniProfiles(configContents)) {
|
|
119
|
+
if (sectionName === profile || sectionName === `profile ${profile}`) {
|
|
120
|
+
Object.assign(merged, values);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const [sectionName, values] of parseIniProfiles(credentialsContents)) {
|
|
124
|
+
if (sectionName === profile) {
|
|
125
|
+
Object.assign(merged, values);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const accessKeyId = merged.aws_access_key_id;
|
|
130
|
+
const secretAccessKey = merged.aws_secret_access_key;
|
|
131
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
accessKeyId,
|
|
136
|
+
secretAccessKey,
|
|
137
|
+
sessionToken: merged.aws_session_token,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveAwsHome(): string {
|
|
142
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveAwsFilePath(variable: string, fallback: string): string {
|
|
146
|
+
const configured = process.env[variable] || fallback;
|
|
147
|
+
return configured.startsWith("~/") ? join(resolveAwsHome(), configured.slice(2)) : configured;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function loadCaseInsensitiveStaticCredentials(
|
|
151
|
+
profileName: string,
|
|
152
|
+
): Promise<StaticAwsCredentials | null> {
|
|
153
|
+
const home = resolveAwsHome();
|
|
154
|
+
const configPath = resolveAwsFilePath("AWS_CONFIG_FILE", join(home, ".aws", "config"));
|
|
155
|
+
const credentialsPath = resolveAwsFilePath(
|
|
156
|
+
"AWS_SHARED_CREDENTIALS_FILE",
|
|
157
|
+
join(home, ".aws", "credentials"),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const [configResult, credentialsResult] = await Promise.allSettled([
|
|
161
|
+
readFile(configPath, "utf8"),
|
|
162
|
+
readFile(credentialsPath, "utf8"),
|
|
163
|
+
]);
|
|
164
|
+
const configContents = configResult.status === "fulfilled" ? configResult.value : "";
|
|
165
|
+
const credentialsContents =
|
|
166
|
+
credentialsResult.status === "fulfilled" ? credentialsResult.value : "";
|
|
167
|
+
return parseCaseInsensitiveStaticCredentials(configContents, credentialsContents, profileName);
|
|
168
|
+
}
|
|
169
|
+
|
|
57
170
|
/**
|
|
58
171
|
* Reads MIRU_SAGEMAKER_ENDPOINT_ARN (or MIRU_SAGEMAKER_ENDPOINT_NAME + a region) to enable
|
|
59
172
|
* self-hosted mode. AWS credentials are resolved by the SDK's default provider chain
|
|
@@ -91,6 +204,7 @@ export function resolveSageMakerConfig(): SageMakerEmbeddingConfig | null {
|
|
|
91
204
|
return {
|
|
92
205
|
endpointName,
|
|
93
206
|
region,
|
|
207
|
+
profile: process.env.AWS_PROFILE?.trim() || undefined,
|
|
94
208
|
normalize: envBool("MIRU_SAGEMAKER_NORMALIZE", true),
|
|
95
209
|
truncate: envBool("MIRU_SAGEMAKER_TRUNCATE", true),
|
|
96
210
|
truncationDirection: resolveTruncationDirection(),
|
|
@@ -108,6 +222,18 @@ export const SAGEMAKER_AUTH_ERROR_MESSAGE =
|
|
|
108
222
|
"(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN or AWS_PROFILE) and that the " +
|
|
109
223
|
"identity has sagemaker:InvokeEndpoint permission on the endpoint.";
|
|
110
224
|
|
|
225
|
+
/** Shown when the endpoint name in the stored/env config no longer exists. */
|
|
226
|
+
export const SAGEMAKER_NOT_FOUND_ERROR_MESSAGE =
|
|
227
|
+
"SageMaker endpoint not found — it may have been deleted, renamed, or deployed in a " +
|
|
228
|
+
"different region. Confirm the endpoint still exists, then run `miru setup --sagemaker` " +
|
|
229
|
+
"again to re-authenticate and re-point Miru at the correct endpoint.";
|
|
230
|
+
|
|
231
|
+
/** Shown for DNS/connection-level failures reaching the SageMaker Runtime API itself. */
|
|
232
|
+
export const SAGEMAKER_UNREACHABLE_ERROR_MESSAGE =
|
|
233
|
+
"Could not reach the SageMaker endpoint (network error). Check your network/VPN/VPC " +
|
|
234
|
+
"access and that the configured region is correct, then run `miru setup --sagemaker` " +
|
|
235
|
+
"again to re-authenticate and re-validate the connection.";
|
|
236
|
+
|
|
111
237
|
const AUTH_EXCEPTION_NAMES = new Set([
|
|
112
238
|
"AccessDeniedException",
|
|
113
239
|
"UnrecognizedClientException",
|
|
@@ -115,6 +241,15 @@ const AUTH_EXCEPTION_NAMES = new Set([
|
|
|
115
241
|
"CredentialsProviderError",
|
|
116
242
|
]);
|
|
117
243
|
|
|
244
|
+
/** Node/undici connection-level error codes — the request never reached AWS. */
|
|
245
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
246
|
+
"ENOTFOUND",
|
|
247
|
+
"ECONNREFUSED",
|
|
248
|
+
"ECONNRESET",
|
|
249
|
+
"ETIMEDOUT",
|
|
250
|
+
"EAI_AGAIN",
|
|
251
|
+
]);
|
|
252
|
+
|
|
118
253
|
class SageMakerInvokeError extends Error {
|
|
119
254
|
readonly status: number;
|
|
120
255
|
|
|
@@ -125,10 +260,11 @@ class SageMakerInvokeError extends Error {
|
|
|
125
260
|
}
|
|
126
261
|
}
|
|
127
262
|
|
|
128
|
-
function toInvokeError(err: unknown): SageMakerInvokeError {
|
|
263
|
+
export function toInvokeError(err: unknown): SageMakerInvokeError {
|
|
129
264
|
const awsErr = err as {
|
|
130
265
|
name?: string;
|
|
131
266
|
message?: string;
|
|
267
|
+
code?: string;
|
|
132
268
|
$metadata?: { httpStatusCode?: number };
|
|
133
269
|
OriginalStatusCode?: string | number;
|
|
134
270
|
OriginalMessage?: string;
|
|
@@ -138,6 +274,17 @@ function toInvokeError(err: unknown): SageMakerInvokeError {
|
|
|
138
274
|
return new SageMakerInvokeError(403, SAGEMAKER_AUTH_ERROR_MESSAGE);
|
|
139
275
|
}
|
|
140
276
|
|
|
277
|
+
// SageMaker returns a generic ValidationError for both "endpoint doesn't exist" and
|
|
278
|
+
// malformed-request cases — only the message distinguishes them.
|
|
279
|
+
if (awsErr?.name === "ValidationError" && /not found/i.test(awsErr?.message ?? "")) {
|
|
280
|
+
return new SageMakerInvokeError(404, SAGEMAKER_NOT_FOUND_ERROR_MESSAGE);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const errorCode = awsErr?.code ?? (err as { cause?: { code?: string } })?.cause?.code;
|
|
284
|
+
if (errorCode && NETWORK_ERROR_CODES.has(errorCode)) {
|
|
285
|
+
return new SageMakerInvokeError(503, SAGEMAKER_UNREACHABLE_ERROR_MESSAGE);
|
|
286
|
+
}
|
|
287
|
+
|
|
141
288
|
// ModelError wraps the embedding container's real status/message (e.g. 424 when the
|
|
142
289
|
// deployed model isn't an embedding model) behind SageMaker's own 424 response.
|
|
143
290
|
const originalStatus =
|
|
@@ -206,8 +353,24 @@ export function createSageMakerClient(config: SageMakerEmbeddingConfig): Embeddi
|
|
|
206
353
|
_model: string,
|
|
207
354
|
dimensions?: number,
|
|
208
355
|
): Promise<EmbeddingResponse> {
|
|
209
|
-
const { InvokeEndpointCommand, SageMakerRuntimeClient }
|
|
210
|
-
|
|
356
|
+
const [{ InvokeEndpointCommand, SageMakerRuntimeClient }, { defaultProvider }] =
|
|
357
|
+
await Promise.all([loadSageMakerRuntime(), loadAwsCredentialProvider()]);
|
|
358
|
+
if (!client) {
|
|
359
|
+
const profile = config.profile?.trim() || process.env.AWS_PROFILE?.trim();
|
|
360
|
+
const sdkCredentials = defaultProvider({ profile });
|
|
361
|
+
const credentials = async (): Promise<StaticAwsCredentials> => {
|
|
362
|
+
try {
|
|
363
|
+
return await sdkCredentials();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
const fallback = profile ? await loadCaseInsensitiveStaticCredentials(profile) : null;
|
|
366
|
+
if (fallback) {
|
|
367
|
+
return fallback;
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
client = new SageMakerRuntimeClient({ region: config.region, credentials });
|
|
373
|
+
}
|
|
211
374
|
|
|
212
375
|
const body = buildRequestBody(config, input, dimensions);
|
|
213
376
|
|
package/src/literal.ts
CHANGED
|
@@ -124,7 +124,7 @@ function expandLiterals(literals: readonly string[], matchVariants: boolean): st
|
|
|
124
124
|
return [...seen];
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function filterChunksByGlob(
|
|
127
|
+
export function filterChunksByGlob(
|
|
128
128
|
chunks: readonly Chunk[],
|
|
129
129
|
include: string[] | undefined,
|
|
130
130
|
exclude: string[] | undefined,
|