@hmharness/evolution 0.14.24 → 0.14.26
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/dist/candidates.d.ts +1 -1
- package/dist/cloud-finetune.d.ts +71 -54
- package/dist/cloud-finetune.js +121 -107
- package/dist/evolve.d.ts +1 -1
- package/dist/index.d.ts +26 -26
- package/dist/knowledge.d.ts +1 -1
- package/dist/reward-model.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +2 -2
package/dist/candidates.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BenchCase } from './bench.
|
|
1
|
+
import type { BenchCase } from './bench.js';
|
|
2
2
|
export type CandidateTarget = 'prompt' | 'skill' | 'context' | 'tool_policy' | 'model_router' | 'workflow' | 'harness';
|
|
3
3
|
export declare const CANDIDATE_TARGETS: CandidateTarget[];
|
|
4
4
|
export interface EvolutionCandidate {
|
package/dist/cloud-finetune.d.ts
CHANGED
|
@@ -1,64 +1,46 @@
|
|
|
1
|
-
import type { DpoPair } from './reward-model.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { DpoPair } from './reward-model.js';
|
|
2
|
+
import type { ProviderConfig } from '@hmharness/kernel';
|
|
3
|
+
/** One provider's fine-tuning dialect. Everything provider-specific lives
|
|
4
|
+
* here; the transport and job flow above it are generic. */
|
|
5
|
+
export interface FineTuneBackend {
|
|
5
6
|
id: string;
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/** Chinese, human-facing (shown in `hmh auto-finetune` output). */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Does this provider entry belong to this backend? */
|
|
10
|
+
matches(baseUrl: string): boolean;
|
|
11
|
+
/** Normalize a chat baseUrl into the fine-tuning API root. */
|
|
12
|
+
apiBase(baseUrl: string): string;
|
|
13
|
+
/** Fine-tune base candidates, the user's MAIN model first. */
|
|
14
|
+
modelLadder(mainModel: string): string[];
|
|
15
|
+
/** Serialize pairs into this provider's DPO JSONL. */
|
|
16
|
+
dpoJsonl(pairs: Array<Pick<DpoPair, 'prompt' | 'chosen' | 'rejected'>>): string;
|
|
17
|
+
/** Does this API error text mean "this model is not fine-tunable here"?
|
|
18
|
+
* Used by the zero-side-effect model probe. */
|
|
19
|
+
modelRejected(errorText: string): boolean;
|
|
8
20
|
}
|
|
9
|
-
/**
|
|
10
|
-
* glm-4-air
|
|
11
|
-
export declare const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* anything, and the error text tells us whether the model is usable.
|
|
21
|
-
*/
|
|
22
|
-
export declare function fineTuneModelLadder(mainModel: string): string[];
|
|
23
|
-
/** Zero-side-effect model probe (the request can never create a job - the
|
|
24
|
-
* training_file id is invalid on purpose). "model rejected" vs "reached
|
|
25
|
-
* file validation" is distinguishable in the error text. */
|
|
26
|
-
export declare function probeFineTuneModel(opts: {
|
|
21
|
+
/** Zhipu bigmodel.cn - verified live 2026-09-25 (models glm-5.3 rejected
|
|
22
|
+
* "微调功能未开放", glm-4-air "微调模型不存在", glm-4-flash/glm-4.5-air pass). */
|
|
23
|
+
export declare const ZHIPU_BACKEND: FineTuneBackend;
|
|
24
|
+
/** Registry - one entry per provider dialect, matched against the user's
|
|
25
|
+
* configured providers at runtime. Extend by adding a verified backend. */
|
|
26
|
+
export declare const FINE_TUNE_BACKENDS: FineTuneBackend[];
|
|
27
|
+
/** Kept for the existing tests/callers: the Zhipu row format. */
|
|
28
|
+
export declare const toDpoJsonl: (pairs: Array<Pick<DpoPair, "prompt" | "chosen" | "rejected">>) => string;
|
|
29
|
+
export interface ResolvedFineTune {
|
|
30
|
+
backend: FineTuneBackend;
|
|
31
|
+
providerName: string;
|
|
27
32
|
apiKey: string;
|
|
28
33
|
baseUrl: string;
|
|
34
|
+
/** the user's MAIN model on that provider (first ladder candidate) */
|
|
29
35
|
model: string;
|
|
30
|
-
|
|
31
|
-
}): Promise<{
|
|
32
|
-
usable: boolean;
|
|
33
|
-
reason?: string;
|
|
34
|
-
}>;
|
|
35
|
-
/** Walk the ladder, return the first usable model plus why earlier ones
|
|
36
|
-
* were skipped (surfaced as one Chinese line - the backend adapts, the
|
|
37
|
-
* user reads, never chooses). */
|
|
38
|
-
export declare function pickFineTuneModel(opts: {
|
|
39
|
-
apiKey: string;
|
|
40
|
-
baseUrl: string;
|
|
41
|
-
mainModel: string;
|
|
42
|
-
log?: (line: string) => void;
|
|
43
|
-
fetchImpl?: typeof fetch;
|
|
44
|
-
}): Promise<string>;
|
|
36
|
+
}
|
|
45
37
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
38
|
+
* Match the user's providers against the backend registry. The CHAT route's
|
|
39
|
+
* provider is tried first (fine-tune what you actually use), then any other
|
|
40
|
+
* configured provider. Returns null when nothing matches - the caller
|
|
41
|
+
* surfaces "not supported yet" as a status, never as an error.
|
|
50
42
|
*/
|
|
51
|
-
export declare function
|
|
52
|
-
/** Serialize pairs into the provider DPO JSONL row format (pure). Accepts
|
|
53
|
-
* the slim {prompt,chosen,rejected} rows the CLI persists - provenance
|
|
54
|
-
* fields never leave the machine.
|
|
55
|
-
* Format verified live 2026-09-25: the documented {"input":{"messages":...}}
|
|
56
|
-
* nesting is REJECTED by the upload validator ("缺少messages字段"); the
|
|
57
|
-
* accepted shape is top-level messages (MUST contain an assistant turn -
|
|
58
|
-
* "缺少assistant角色" otherwise) plus preferred_output/non_preferred_output.
|
|
59
|
-
* The assistant turn carries the chosen answer, so the row is a complete
|
|
60
|
-
* good trajectory and the preference pair trains the separation. */
|
|
61
|
-
export declare function toDpoJsonl(pairs: Array<Pick<DpoPair, 'prompt' | 'chosen' | 'rejected'>>): string;
|
|
43
|
+
export declare function resolveFineTuneBackend(providers: Record<string, ProviderConfig> | undefined, chatProviderName?: string): ResolvedFineTune | null;
|
|
62
44
|
/** Rough token estimate for a cost preview before upload (chars/4 heuristic;
|
|
63
45
|
* the upload response returns exact per-tokenizer stats which the UI shows
|
|
64
46
|
* alongside - this is only the pre-flight number). */
|
|
@@ -75,6 +57,7 @@ export interface UploadedFile {
|
|
|
75
57
|
}
|
|
76
58
|
/** Upload a training/validation JSONL (free operation, verified live). */
|
|
77
59
|
export declare function uploadFineTuneFile(opts: {
|
|
60
|
+
backend: FineTuneBackend;
|
|
78
61
|
apiKey: string;
|
|
79
62
|
baseUrl: string;
|
|
80
63
|
filename: string;
|
|
@@ -93,6 +76,7 @@ export interface FineTuneJob {
|
|
|
93
76
|
/** Create a fine-tuning job (PAID - callers must gate behind explicit
|
|
94
77
|
* submit consent; uploads alone are free). */
|
|
95
78
|
export declare function createFineTuneJob(opts: {
|
|
79
|
+
backend: FineTuneBackend;
|
|
96
80
|
apiKey: string;
|
|
97
81
|
baseUrl: string;
|
|
98
82
|
model: string;
|
|
@@ -107,23 +91,56 @@ export declare function createFineTuneJob(opts: {
|
|
|
107
91
|
fetchImpl?: typeof fetch;
|
|
108
92
|
}): Promise<FineTuneJob>;
|
|
109
93
|
export declare function listFineTuneJobs(opts: {
|
|
94
|
+
backend: FineTuneBackend;
|
|
110
95
|
apiKey: string;
|
|
111
96
|
baseUrl: string;
|
|
112
97
|
limit?: number;
|
|
113
98
|
fetchImpl?: typeof fetch;
|
|
114
99
|
}): Promise<FineTuneJob[]>;
|
|
115
100
|
export declare function retrieveFineTuneJob(opts: {
|
|
101
|
+
backend: FineTuneBackend;
|
|
116
102
|
apiKey: string;
|
|
117
103
|
baseUrl: string;
|
|
118
104
|
jobId: string;
|
|
119
105
|
fetchImpl?: typeof fetch;
|
|
120
106
|
}): Promise<FineTuneJob>;
|
|
121
107
|
export declare function cancelFineTuneJob(opts: {
|
|
108
|
+
backend: FineTuneBackend;
|
|
122
109
|
apiKey: string;
|
|
123
110
|
baseUrl: string;
|
|
124
111
|
jobId: string;
|
|
125
112
|
fetchImpl?: typeof fetch;
|
|
126
113
|
}): Promise<FineTuneJob>;
|
|
114
|
+
/**
|
|
115
|
+
* Model auto-selection (user direction 2026-09-25: novices never pick a
|
|
116
|
+
* fine-tune base). Candidates: the user's MAIN model first - if the API
|
|
117
|
+
* rejects it the ladder silently falls to the next documented fine-tunable
|
|
118
|
+
* base. The probe POSTs {model, training_file:"file-probe-invalid"} -
|
|
119
|
+
* model validation runs BEFORE file binding, so the request always fails
|
|
120
|
+
* without creating anything, and the error text tells us whether the model
|
|
121
|
+
* is usable.
|
|
122
|
+
*/
|
|
123
|
+
export declare function probeFineTuneModel(opts: {
|
|
124
|
+
backend: FineTuneBackend;
|
|
125
|
+
apiKey: string;
|
|
126
|
+
baseUrl: string;
|
|
127
|
+
model: string;
|
|
128
|
+
fetchImpl?: typeof fetch;
|
|
129
|
+
}): Promise<{
|
|
130
|
+
usable: boolean;
|
|
131
|
+
reason?: string;
|
|
132
|
+
}>;
|
|
133
|
+
/** Walk the ladder, return the first usable model plus why earlier ones
|
|
134
|
+
* were skipped (surfaced as one Chinese line - the backend adapts, the
|
|
135
|
+
* user reads, never chooses). */
|
|
136
|
+
export declare function pickFineTuneModel(opts: {
|
|
137
|
+
backend: FineTuneBackend;
|
|
138
|
+
apiKey: string;
|
|
139
|
+
baseUrl: string;
|
|
140
|
+
mainModel: string;
|
|
141
|
+
log?: (line: string) => void;
|
|
142
|
+
fetchImpl?: typeof fetch;
|
|
143
|
+
}): Promise<string>;
|
|
127
144
|
/** Append-only record of jobs this machine submitted (audit trail: what was
|
|
128
145
|
* trained, on which file, when - survives provider console churn). */
|
|
129
146
|
export declare function recordFineTuneJob(home: string, job: FineTuneJob, extra?: Record<string, unknown>): Promise<void>;
|
package/dist/cloud-finetune.js
CHANGED
|
@@ -1,118 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cloud fine-tuning client (2026-09-25, closes the auto-finetune loop).
|
|
3
3
|
*
|
|
4
|
-
* Direction (user, 2026-09-24): users configure ONE api key
|
|
5
|
-
* adapts everything else
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Direction (user, 2026-09-24/25): users configure ONE api key for whatever
|
|
5
|
+
* provider they use, the backend adapts everything else. Fine-tuning is
|
|
6
|
+
* therefore a PROVIDER-AGNOSTIC capability: a registry of backends, each
|
|
7
|
+
* knowing one provider's endpoints, accepted data format and model rules.
|
|
8
|
+
* Zhipu is the first backend because it is the only one verified live
|
|
9
|
+
* (2026-09-25, real key, real uploads) - NOT because hmh targets Zhipu.
|
|
10
|
+
* A user on any other provider gets a clean "not supported yet" status,
|
|
11
|
+
* never an error, and everything else keeps working.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* POST /
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* GET /api/paas/v4/fine_tuning/jobs/{id}/events
|
|
17
|
-
* Cross-checked field names against zhipuai SDK 2.1.5 source
|
|
18
|
-
* (api_resource/fine_tuning/jobs/jobs.py). The DPO row format follows the
|
|
19
|
-
* official guide: {"input":{messages,tools,parallel_tool_calls},
|
|
20
|
-
* "preferred_output":[...],"non_preferred_output":[...]}.
|
|
13
|
+
* Zhipu endpoint surface (verified live 2026-09-25):
|
|
14
|
+
* POST {apiBase}/files multipart file + purpose=fine-tune
|
|
15
|
+
* POST {apiBase}/fine_tuning/jobs {model, training_file, ...}
|
|
16
|
+
* GET {apiBase}/fine_tuning/jobs list (+ /{id}, /{id}/cancel, /{id}/events)
|
|
17
|
+
* Cross-checked field names against zhipuai SDK 2.1.5 source. The DPO row
|
|
18
|
+
* format is the LIVE-verified one, which differs from Zhipu's own docs
|
|
19
|
+
* (see zhipuDpoJsonl).
|
|
21
20
|
*/
|
|
22
21
|
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
23
22
|
import { join } from 'node:path';
|
|
24
|
-
|
|
25
|
-
* glm-4-air 404s "微调模型不存在", glm-4-flash passes model validation). */
|
|
26
|
-
export const FINE_TUNE_CATALOG = [
|
|
27
|
-
{ id: 'glm-4-flash', dpo: true, note: '最便宜,LoRA 需开发者 Pro 权益' },
|
|
28
|
-
{ id: 'glm-4.5-air', dpo: false, note: '全参微调,对所有用户开放(SFT)' },
|
|
29
|
-
{ id: 'glm-4-air-250414', dpo: true, note: '全参微调' },
|
|
30
|
-
{ id: 'glm-4-air-x', dpo: true, note: '全参微调' },
|
|
31
|
-
];
|
|
32
|
-
export const DEFAULT_FINE_TUNE_MODEL = 'glm-4-flash';
|
|
33
|
-
/**
|
|
34
|
-
* Model auto-selection (user direction 2026-09-25: novices must never pick
|
|
35
|
-
* a fine-tune base). Candidates: the user's MAIN model first - if the API
|
|
36
|
-
* rejects it (live probe 2026-09-25: glm-5.3 answers "微调功能未开放") the
|
|
37
|
-
* ladder silently falls to the next documented fine-tunable base. The probe
|
|
38
|
-
* POSTs {model, training_file:"file-probe-invalid"} - model validation runs
|
|
39
|
-
* BEFORE file binding, so the request always fails without creating
|
|
40
|
-
* anything, and the error text tells us whether the model is usable.
|
|
41
|
-
*/
|
|
42
|
-
export function fineTuneModelLadder(mainModel) {
|
|
43
|
-
const ladder = [mainModel, DEFAULT_FINE_TUNE_MODEL, 'glm-4.5-air'];
|
|
44
|
-
return [...new Set(ladder.filter(Boolean))];
|
|
45
|
-
}
|
|
46
|
-
/** Zero-side-effect model probe (the request can never create a job - the
|
|
47
|
-
* training_file id is invalid on purpose). "model rejected" vs "reached
|
|
48
|
-
* file validation" is distinguishable in the error text. */
|
|
49
|
-
export async function probeFineTuneModel(opts) {
|
|
50
|
-
const url = normalizeFineTuneBase(opts.baseUrl) + '/fine_tuning/jobs';
|
|
51
|
-
const res = await (opts.fetchImpl ?? fetch)(url, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: { Authorization: `Bearer ${opts.apiKey}`, 'Content-Type': 'application/json' },
|
|
54
|
-
body: JSON.stringify({ model: opts.model, training_file: 'file-probe-invalid' }),
|
|
55
|
-
});
|
|
56
|
-
const text = await res.text();
|
|
57
|
-
if (res.ok)
|
|
58
|
-
return { usable: true }; // unreachable in practice; harmless
|
|
59
|
-
const rejected = /微调模型不存在|微调功能未开放|无权限|not found/i.test(text);
|
|
60
|
-
return { usable: !rejected, reason: rejected ? text.slice(0, 160) : undefined };
|
|
61
|
-
}
|
|
62
|
-
/** Walk the ladder, return the first usable model plus why earlier ones
|
|
63
|
-
* were skipped (surfaced as one Chinese line - the backend adapts, the
|
|
64
|
-
* user reads, never chooses). */
|
|
65
|
-
export async function pickFineTuneModel(opts) {
|
|
66
|
-
for (const m of fineTuneModelLadder(opts.mainModel)) {
|
|
67
|
-
const r = await probeFineTuneModel({ apiKey: opts.apiKey, baseUrl: opts.baseUrl, model: m, fetchImpl: opts.fetchImpl });
|
|
68
|
-
if (r.usable)
|
|
69
|
-
return m;
|
|
70
|
-
opts.log?.(`微调候选 ${m} 不可用(${(r.reason ?? '').replace(/^.*"message":"([^"]*)".*$/, '$1').slice(0, 60) || 'API 拒绝'}),自动改试下一个`);
|
|
71
|
-
}
|
|
72
|
-
throw new Error('所有微调候选模型均不可用——请在智谱控制台确认微调权限(可能需联系客服开放),或用 --model= 指定其他可微调模型');
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Fine-tuning does not live on the coding endpoint: config.json points the
|
|
76
|
-
* chat route at /api/coding/paas/v4, but the fine_tuning/file APIs sit on
|
|
77
|
-
* /api/paas/v4. Strip the coding segment so the user's existing glm provider
|
|
78
|
-
* entry works unchanged (zero new configuration).
|
|
79
|
-
*/
|
|
80
|
-
export function normalizeFineTuneBase(baseUrl) {
|
|
81
|
-
let u = baseUrl.trim().replace(/\/+$/, '');
|
|
82
|
-
u = u.replace(/\/coding\/paas\/v4$/, '/paas/v4');
|
|
83
|
-
if (!/\/api\/paas\/v4$/.test(u)) {
|
|
84
|
-
const m = u.match(/^(https?:\/\/[^/]+)/);
|
|
85
|
-
if (!m)
|
|
86
|
-
throw new Error('baseUrl 需以 http(s):// 开头: ' + baseUrl);
|
|
87
|
-
u = m[1] + '/api/paas/v4';
|
|
88
|
-
}
|
|
89
|
-
return u;
|
|
90
|
-
}
|
|
91
|
-
/** Serialize pairs into the provider DPO JSONL row format (pure). Accepts
|
|
92
|
-
* the slim {prompt,chosen,rejected} rows the CLI persists - provenance
|
|
93
|
-
* fields never leave the machine.
|
|
94
|
-
* Format verified live 2026-09-25: the documented {"input":{"messages":...}}
|
|
95
|
-
* nesting is REJECTED by the upload validator ("缺少messages字段"); the
|
|
96
|
-
* accepted shape is top-level messages (MUST contain an assistant turn -
|
|
97
|
-
* "缺少assistant角色" otherwise) plus preferred_output/non_preferred_output.
|
|
98
|
-
* The assistant turn carries the chosen answer, so the row is a complete
|
|
99
|
-
* good trajectory and the preference pair trains the separation. */
|
|
100
|
-
export function toDpoJsonl(pairs) {
|
|
23
|
+
const zhipuDpoJsonl = (pairs) => {
|
|
101
24
|
const lines = [];
|
|
102
25
|
for (const p of pairs) {
|
|
103
26
|
if (!p.prompt.trim() || !p.chosen.trim() || !p.rejected.trim())
|
|
104
27
|
continue; // quality gate mirrors rl-governance
|
|
105
|
-
|
|
28
|
+
// Live-verified 2026-09-25: the documented {"input":{"messages":...}}
|
|
29
|
+
// nesting is REJECTED by the upload validator ("缺少messages字段"); the
|
|
30
|
+
// accepted shape is top-level messages (MUST contain an assistant turn -
|
|
31
|
+
// "缺少assistant角色" otherwise) plus preferred_output/non_preferred_output.
|
|
32
|
+
// The assistant turn carries the chosen answer, so the row is a complete
|
|
33
|
+
// good trajectory and the preference pair trains the separation.
|
|
34
|
+
lines.push(JSON.stringify({
|
|
106
35
|
messages: [
|
|
107
36
|
{ role: 'user', content: p.prompt },
|
|
108
37
|
{ role: 'assistant', content: p.chosen },
|
|
109
38
|
],
|
|
110
39
|
preferred_output: [{ role: 'assistant', content: p.chosen }],
|
|
111
40
|
non_preferred_output: [{ role: 'assistant', content: p.rejected }],
|
|
112
|
-
};
|
|
113
|
-
lines.push(JSON.stringify(row));
|
|
41
|
+
}));
|
|
114
42
|
}
|
|
115
43
|
return lines.join('\n') + (lines.length ? '\n' : '');
|
|
44
|
+
};
|
|
45
|
+
/** Zhipu bigmodel.cn - verified live 2026-09-25 (models glm-5.3 rejected
|
|
46
|
+
* "微调功能未开放", glm-4-air "微调模型不存在", glm-4-flash/glm-4.5-air pass). */
|
|
47
|
+
export const ZHIPU_BACKEND = {
|
|
48
|
+
id: 'zhipu',
|
|
49
|
+
label: '智谱(bigmodel.cn)',
|
|
50
|
+
matches: (baseUrl) => baseUrl.includes('bigmodel.cn'),
|
|
51
|
+
apiBase: (baseUrl) => {
|
|
52
|
+
let u = baseUrl.trim().replace(/\/+$/, '');
|
|
53
|
+
u = u.replace(/\/coding\/paas\/v4$/, '/paas/v4');
|
|
54
|
+
if (!/\/api\/paas\/v4$/.test(u)) {
|
|
55
|
+
const m = u.match(/^(https?:\/\/[^/]+)/);
|
|
56
|
+
if (!m)
|
|
57
|
+
throw new Error('baseUrl 需以 http(s):// 开头: ' + baseUrl);
|
|
58
|
+
u = m[1] + '/api/paas/v4';
|
|
59
|
+
}
|
|
60
|
+
return u;
|
|
61
|
+
},
|
|
62
|
+
modelLadder: (mainModel) => [...new Set([mainModel, 'glm-4-flash', 'glm-4.5-air'].filter(Boolean))],
|
|
63
|
+
dpoJsonl: zhipuDpoJsonl,
|
|
64
|
+
modelRejected: (text) => /微调模型不存在|微调功能未开放|无权限|not found/i.test(text),
|
|
65
|
+
};
|
|
66
|
+
/** Registry - one entry per provider dialect, matched against the user's
|
|
67
|
+
* configured providers at runtime. Extend by adding a verified backend. */
|
|
68
|
+
export const FINE_TUNE_BACKENDS = [ZHIPU_BACKEND];
|
|
69
|
+
/** Kept for the existing tests/callers: the Zhipu row format. */
|
|
70
|
+
export const toDpoJsonl = zhipuDpoJsonl;
|
|
71
|
+
/**
|
|
72
|
+
* Match the user's providers against the backend registry. The CHAT route's
|
|
73
|
+
* provider is tried first (fine-tune what you actually use), then any other
|
|
74
|
+
* configured provider. Returns null when nothing matches - the caller
|
|
75
|
+
* surfaces "not supported yet" as a status, never as an error.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveFineTuneBackend(providers, chatProviderName) {
|
|
78
|
+
if (!providers)
|
|
79
|
+
return null;
|
|
80
|
+
const ordered = [];
|
|
81
|
+
if (chatProviderName && providers[chatProviderName])
|
|
82
|
+
ordered.push([chatProviderName, providers[chatProviderName]]);
|
|
83
|
+
for (const [name, p] of Object.entries(providers)) {
|
|
84
|
+
if (!ordered.some(([n]) => n === name))
|
|
85
|
+
ordered.push([name, p]);
|
|
86
|
+
}
|
|
87
|
+
for (const [name, p] of ordered) {
|
|
88
|
+
if (!p.baseUrl || !p.apiKey)
|
|
89
|
+
continue;
|
|
90
|
+
const backend = FINE_TUNE_BACKENDS.find((b) => b.matches(p.baseUrl));
|
|
91
|
+
if (backend)
|
|
92
|
+
return { backend, providerName: name, apiKey: p.apiKey, baseUrl: p.baseUrl, model: p.model };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
116
95
|
}
|
|
117
96
|
/** Rough token estimate for a cost preview before upload (chars/4 heuristic;
|
|
118
97
|
* the upload response returns exact per-tokenizer stats which the UI shows
|
|
@@ -125,7 +104,7 @@ export function estimateTokens(jsonl) {
|
|
|
125
104
|
export function estimateTrainingCost(tokens, epochs, pricePerKTokens) {
|
|
126
105
|
return Math.round(((tokens / 1000) * epochs * pricePerKTokens) * 100) / 100;
|
|
127
106
|
}
|
|
128
|
-
async function
|
|
107
|
+
async function apiFetch(url, apiKey, init, fetchImpl) {
|
|
129
108
|
const res = await fetchImpl(url, {
|
|
130
109
|
...init,
|
|
131
110
|
headers: { Authorization: `Bearer ${apiKey}`, ...init.headers },
|
|
@@ -146,11 +125,11 @@ async function zhipuFetch(url, apiKey, init, fetchImpl) {
|
|
|
146
125
|
}
|
|
147
126
|
/** Upload a training/validation JSONL (free operation, verified live). */
|
|
148
127
|
export async function uploadFineTuneFile(opts) {
|
|
149
|
-
const url =
|
|
128
|
+
const url = opts.backend.apiBase(opts.baseUrl) + '/files';
|
|
150
129
|
const form = new FormData();
|
|
151
130
|
form.append('purpose', 'fine-tune');
|
|
152
131
|
form.append('file', new Blob([opts.jsonl], { type: 'application/jsonl' }), opts.filename);
|
|
153
|
-
const body = await
|
|
132
|
+
const body = await apiFetch(url, opts.apiKey, { method: 'POST', body: form }, opts.fetchImpl ?? fetch);
|
|
154
133
|
const o = body;
|
|
155
134
|
const tokens = Math.max(0, ...(o.text_stats ?? []).map((t) => t.tokens ?? 0));
|
|
156
135
|
return { id: o.id, bytes: o.bytes, samples: o.samples ?? 0, tokens };
|
|
@@ -158,7 +137,7 @@ export async function uploadFineTuneFile(opts) {
|
|
|
158
137
|
/** Create a fine-tuning job (PAID - callers must gate behind explicit
|
|
159
138
|
* submit consent; uploads alone are free). */
|
|
160
139
|
export async function createFineTuneJob(opts) {
|
|
161
|
-
const url =
|
|
140
|
+
const url = opts.backend.apiBase(opts.baseUrl) + '/fine_tuning/jobs';
|
|
162
141
|
const payload = { model: opts.model, training_file: opts.trainingFile };
|
|
163
142
|
if (opts.validationFile)
|
|
164
143
|
payload.validation_file = opts.validationFile;
|
|
@@ -166,24 +145,59 @@ export async function createFineTuneJob(opts) {
|
|
|
166
145
|
payload.suffix = opts.suffix;
|
|
167
146
|
if (opts.hyperparameters)
|
|
168
147
|
payload.hyperparameters = opts.hyperparameters;
|
|
169
|
-
return (await
|
|
148
|
+
return (await apiFetch(url, opts.apiKey, {
|
|
170
149
|
method: 'POST',
|
|
171
150
|
headers: { 'Content-Type': 'application/json' },
|
|
172
151
|
body: JSON.stringify(payload),
|
|
173
152
|
}, opts.fetchImpl ?? fetch));
|
|
174
153
|
}
|
|
175
154
|
export async function listFineTuneJobs(opts) {
|
|
176
|
-
const url =
|
|
177
|
-
const body = await
|
|
155
|
+
const url = opts.backend.apiBase(opts.baseUrl) + `/fine_tuning/jobs?limit=${opts.limit ?? 10}`;
|
|
156
|
+
const body = await apiFetch(url, opts.apiKey, { method: 'GET' }, opts.fetchImpl ?? fetch);
|
|
178
157
|
return (body.data ?? []);
|
|
179
158
|
}
|
|
180
159
|
export async function retrieveFineTuneJob(opts) {
|
|
181
|
-
const url =
|
|
182
|
-
return (await
|
|
160
|
+
const url = opts.backend.apiBase(opts.baseUrl) + `/fine_tuning/jobs/${opts.jobId}`;
|
|
161
|
+
return (await apiFetch(url, opts.apiKey, { method: 'GET' }, opts.fetchImpl ?? fetch));
|
|
183
162
|
}
|
|
184
163
|
export async function cancelFineTuneJob(opts) {
|
|
185
|
-
const url =
|
|
186
|
-
return (await
|
|
164
|
+
const url = opts.backend.apiBase(opts.baseUrl) + `/fine_tuning/jobs/${opts.jobId}/cancel`;
|
|
165
|
+
return (await apiFetch(url, opts.apiKey, { method: 'POST' }, opts.fetchImpl ?? fetch));
|
|
166
|
+
}
|
|
167
|
+
/* ---------------- model auto-selection ---------------- */
|
|
168
|
+
/**
|
|
169
|
+
* Model auto-selection (user direction 2026-09-25: novices never pick a
|
|
170
|
+
* fine-tune base). Candidates: the user's MAIN model first - if the API
|
|
171
|
+
* rejects it the ladder silently falls to the next documented fine-tunable
|
|
172
|
+
* base. The probe POSTs {model, training_file:"file-probe-invalid"} -
|
|
173
|
+
* model validation runs BEFORE file binding, so the request always fails
|
|
174
|
+
* without creating anything, and the error text tells us whether the model
|
|
175
|
+
* is usable.
|
|
176
|
+
*/
|
|
177
|
+
export async function probeFineTuneModel(opts) {
|
|
178
|
+
const url = opts.backend.apiBase(opts.baseUrl) + '/fine_tuning/jobs';
|
|
179
|
+
const res = await (opts.fetchImpl ?? fetch)(url, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ model: opts.model, training_file: 'file-probe-invalid' }),
|
|
183
|
+
});
|
|
184
|
+
const text = await res.text();
|
|
185
|
+
if (res.ok)
|
|
186
|
+
return { usable: true }; // unreachable in practice; harmless
|
|
187
|
+
const rejected = opts.backend.modelRejected(text);
|
|
188
|
+
return { usable: !rejected, reason: rejected ? text.slice(0, 160) : undefined };
|
|
189
|
+
}
|
|
190
|
+
/** Walk the ladder, return the first usable model plus why earlier ones
|
|
191
|
+
* were skipped (surfaced as one Chinese line - the backend adapts, the
|
|
192
|
+
* user reads, never chooses). */
|
|
193
|
+
export async function pickFineTuneModel(opts) {
|
|
194
|
+
for (const m of opts.backend.modelLadder(opts.mainModel)) {
|
|
195
|
+
const r = await probeFineTuneModel({ backend: opts.backend, apiKey: opts.apiKey, baseUrl: opts.baseUrl, model: m, fetchImpl: opts.fetchImpl });
|
|
196
|
+
if (r.usable)
|
|
197
|
+
return m;
|
|
198
|
+
opts.log?.(`微调候选 ${m} 不可用(${(r.reason ?? '').replace(/^.*"message":"([^"]*)".*$/, '$1').slice(0, 60) || 'API 拒绝'}),自动改试下一个`);
|
|
199
|
+
}
|
|
200
|
+
throw new Error('所有微调候选模型均不可用——请在服务商控制台确认微调权限(可能需联系客服开放),或用 --model= 指定其他可微调模型');
|
|
187
201
|
}
|
|
188
202
|
/* ---------------- local job ledger ---------------- */
|
|
189
203
|
/** Append-only record of jobs this machine submitted (audit trail: what was
|
package/dist/evolve.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
export * from './memory.
|
|
2
|
-
export * from './ranker.
|
|
3
|
-
export * from './candidates.
|
|
4
|
-
export * from './skillpayload.
|
|
5
|
-
export * from './dataset.
|
|
6
|
-
export * from './readiness.
|
|
7
|
-
export * from './labels.
|
|
8
|
-
export * from './reward-model.
|
|
9
|
-
export * from './judge.
|
|
10
|
-
export * from './sequential.
|
|
11
|
-
export * from './insights.
|
|
12
|
-
export * from './skills.
|
|
13
|
-
export * from './bench.
|
|
14
|
-
export * from './evolve.
|
|
15
|
-
export * from './patches.
|
|
16
|
-
export * from './impact.
|
|
17
|
-
export * from './workflows.
|
|
18
|
-
export * from './knowledge.
|
|
19
|
-
export * from './radar.
|
|
20
|
-
export * from './external.
|
|
21
|
-
export * from './knowledge-graph.
|
|
22
|
-
export * from './router-evolution.
|
|
23
|
-
export * from './context-evolution.
|
|
24
|
-
export * from './rl-governance.
|
|
25
|
-
export * from './policy-optimizer.
|
|
26
|
-
export * from './cloud-finetune.
|
|
1
|
+
export * from './memory.js';
|
|
2
|
+
export * from './ranker.js';
|
|
3
|
+
export * from './candidates.js';
|
|
4
|
+
export * from './skillpayload.js';
|
|
5
|
+
export * from './dataset.js';
|
|
6
|
+
export * from './readiness.js';
|
|
7
|
+
export * from './labels.js';
|
|
8
|
+
export * from './reward-model.js';
|
|
9
|
+
export * from './judge.js';
|
|
10
|
+
export * from './sequential.js';
|
|
11
|
+
export * from './insights.js';
|
|
12
|
+
export * from './skills.js';
|
|
13
|
+
export * from './bench.js';
|
|
14
|
+
export * from './evolve.js';
|
|
15
|
+
export * from './patches.js';
|
|
16
|
+
export * from './impact.js';
|
|
17
|
+
export * from './workflows.js';
|
|
18
|
+
export * from './knowledge.js';
|
|
19
|
+
export * from './radar.js';
|
|
20
|
+
export * from './external.js';
|
|
21
|
+
export * from './knowledge-graph.js';
|
|
22
|
+
export * from './router-evolution.js';
|
|
23
|
+
export * from './context-evolution.js';
|
|
24
|
+
export * from './rl-governance.js';
|
|
25
|
+
export * from './policy-optimizer.js';
|
|
26
|
+
export * from './cloud-finetune.js';
|
package/dist/knowledge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProviderConfig } from '@hmharness/kernel';
|
|
2
|
-
import { type SkillProposal } from './evolve.
|
|
2
|
+
import { type SkillProposal } from './evolve.js';
|
|
3
3
|
declare function fetchPage(url: string): Promise<string | null>;
|
|
4
4
|
/** One refresh cycle: fetch -> compare -> maybe one knowledge draft.
|
|
5
5
|
* Returns a short human-readable summary (or 'offline' etc.). */
|
package/dist/reward-model.d.ts
CHANGED
package/dist/workflows.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* a workflow is just a skill with a shape.
|
|
11
11
|
*/
|
|
12
12
|
import { type ProviderConfig } from '@hmharness/kernel';
|
|
13
|
-
import { type SkillProposal } from './evolve.
|
|
13
|
+
import { type SkillProposal } from './evolve.js';
|
|
14
14
|
export interface WorkflowCluster {
|
|
15
15
|
key: string;
|
|
16
16
|
tasks: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.26",
|
|
4
4
|
"description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/kernel": "
|
|
18
|
+
"@hmharness/kernel": "*"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|