@copilotkit/aimock 1.42.1 → 1.44.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +65 -0
- package/README.md +108 -16
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +1 -1
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +44 -12
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +46 -14
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +625 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +616 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +131 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +132 -31
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +37 -9
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +39 -11
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +1 -1
- package/dist/config-loader.js +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +26 -5
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +27 -6
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1069 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1056 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1033 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1026 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +131 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +131 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +44 -4
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +46 -6
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +51 -7
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +53 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +19 -8
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +21 -10
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +497 -10
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +487 -12
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +57 -125
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +58 -126
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +71 -9
- package/dist/index.d.cts +17 -7
- package/dist/index.d.ts +17 -7
- package/dist/index.js +21 -11
- package/dist/jest.cjs +24 -12
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +6 -3
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +6 -3
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +25 -13
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +61 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +60 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +60 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +61 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +132 -49
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +133 -50
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +216 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +210 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes.cjs +1619 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +101 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +101 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1614 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +331 -17
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +330 -18
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +217 -49
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +66 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +66 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +218 -50
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +49 -7
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +51 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +278 -45
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +17 -7
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +17 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +270 -29
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +10 -6
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +11 -7
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +9 -9
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +10 -10
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +54 -10
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +52 -12
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +47 -7
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +48 -9
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +21 -34
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -35
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1252 -777
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1185 -710
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +6 -4
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +7 -5
- package/dist/speech.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +6 -6
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +7 -7
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +320 -17
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +320 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +983 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +982 -0
- package/dist/validate-cli.js.map +1 -0
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +23 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +24 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +23 -11
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +5 -2
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +5 -2
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +25 -13
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +16 -4
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +16 -4
- package/dist/ws-responses.js.map +1 -1
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +4 -1
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
const require_helpers = require('./helpers.cjs');
|
|
2
|
+
const require_chaos = require('./chaos.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/fine-tuning.ts
|
|
5
|
+
/**
|
|
6
|
+
* The statuses a job never leaves. Single source of truth: both `advance()`
|
|
7
|
+
* (which refuses to move a terminal job) and the cancel handler (which rejects
|
|
8
|
+
* a terminal job) read this set, so the two cannot drift apart again.
|
|
9
|
+
*/
|
|
10
|
+
const TERMINAL_STATUSES = new Set([
|
|
11
|
+
"succeeded",
|
|
12
|
+
"cancelled",
|
|
13
|
+
"failed"
|
|
14
|
+
]);
|
|
15
|
+
function isTerminal(status) {
|
|
16
|
+
return TERMINAL_STATUSES.has(status);
|
|
17
|
+
}
|
|
18
|
+
/** Stable stand-in for the owning org; the vendor requires the field, not a value. */
|
|
19
|
+
const ORGANIZATION_ID = "org-aimock";
|
|
20
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
21
|
+
const polls = /* @__PURE__ */ new Map();
|
|
22
|
+
/**
|
|
23
|
+
* The append-only event log per job id, kept beside the job rather than on it
|
|
24
|
+
* so it never leaks into the `fine_tuning.job` payload. `appendEvent` is the
|
|
25
|
+
* only writer of entries, and only by pushing, so an event's index — and
|
|
26
|
+
* therefore its id — is fixed the moment it is recorded. The one other writer
|
|
27
|
+
* is `clearFineTuningStore`, which empties the whole map: a reset drops every
|
|
28
|
+
* log wholesale and never renumbers a surviving one.
|
|
29
|
+
*/
|
|
30
|
+
const eventLogs = /* @__PURE__ */ new Map();
|
|
31
|
+
/**
|
|
32
|
+
* The last second this module stamped, which is what `stamp()` reads and
|
|
33
|
+
* writes. Module-wide rather than per job: a per-job clamp says nothing about
|
|
34
|
+
* two different jobs, and ordering ACROSS jobs is what the list endpoint's
|
|
35
|
+
* newest-first page rests on.
|
|
36
|
+
*/
|
|
37
|
+
let lastStamp = 0;
|
|
38
|
+
/**
|
|
39
|
+
* The one clock the whole surface stamps from.
|
|
40
|
+
*
|
|
41
|
+
* The invariant: every timestamp this module ever exposes - every job's
|
|
42
|
+
* `created_at`, every event's `created_at`, and every `finished_at` - is
|
|
43
|
+
* non-decreasing in the order the module produced it, whichever way the wall
|
|
44
|
+
* clock moves between two stamps, and across all jobs rather than within one.
|
|
45
|
+
* The per-job reading is a consequence: a job's own stamps are a subsequence
|
|
46
|
+
* of the module's, so they are non-decreasing too.
|
|
47
|
+
*
|
|
48
|
+
* Wall clock is the source, so a forward-running clock shows through
|
|
49
|
+
* unchanged; a backwards step (NTP correction, a resumed VM, a test driving
|
|
50
|
+
* `Date.now`) is absorbed by holding the previous value rather than emitting a
|
|
51
|
+
* history that reads out of order. That makes this the only place a
|
|
52
|
+
* fine-tuning timestamp may come from - a second clamp somewhere downstream is
|
|
53
|
+
* exactly what used to be able to undo this one.
|
|
54
|
+
*/
|
|
55
|
+
function stamp() {
|
|
56
|
+
lastStamp = Math.max(Math.floor(Date.now() / 1e3), lastStamp);
|
|
57
|
+
return lastStamp;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The `suffix` the create body asked for, per job id. Kept beside the job
|
|
61
|
+
* because `suffix` is a member of `CreateFineTuningJobRequest` and NOT of
|
|
62
|
+
* `fine_tuning.job`: storing it on the job would put a field on the wire that
|
|
63
|
+
* the vendor never sends. It is read once, when the job succeeds and takes its
|
|
64
|
+
* `fine_tuned_model` name.
|
|
65
|
+
*/
|
|
66
|
+
const suffixes = /* @__PURE__ */ new Map();
|
|
67
|
+
function clearFineTuningStore() {
|
|
68
|
+
jobs.clear();
|
|
69
|
+
polls.clear();
|
|
70
|
+
eventLogs.clear();
|
|
71
|
+
lastStamp = 0;
|
|
72
|
+
suffixes.clear();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Journal one fine-tuning request.
|
|
76
|
+
*
|
|
77
|
+
* `body` is the request body the handler actually parsed (create), and is null
|
|
78
|
+
* in three cases: the bodyless GET/cancel routes, a create whose body never
|
|
79
|
+
* parsed into a JSON object, and any request the chaos gate faulted — `chaosHit`
|
|
80
|
+
* journals through `applyChaosAsync` with `body: null` because it rolls before
|
|
81
|
+
* the body is read at all. Recording it otherwise is what makes "inspect the
|
|
82
|
+
* request you sent" work against this surface: every create that got as far as
|
|
83
|
+
* a parsed JSON object carries it, every rejection included, because those are
|
|
84
|
+
* exactly the 400s whose offending payload a caller needs to read back.
|
|
85
|
+
*
|
|
86
|
+
* `path` is the raw request URL, query string INCLUDED. That is not cosmetic:
|
|
87
|
+
* `entry.path` is the only place an untagged-by-header `?testId=` survives into
|
|
88
|
+
* the journal, and `journalEntryTestId` (server.ts) parses it back out of
|
|
89
|
+
* `entry.path` to serve `GET /__aimock/journal?testId=…`. Stripping the query
|
|
90
|
+
* here — as `fal.ts` does with its `pathname` — silently drops every
|
|
91
|
+
* query-tagged fine-tuning request out of its own test's slice. It also matches
|
|
92
|
+
* the repo majority (images, responses, messages, moderation, embeddings,
|
|
93
|
+
* rerank, cohere, gemini, search and vector all journal `req.url`). A caller
|
|
94
|
+
* that wants route-level grouping should match on `service` or on a prefix,
|
|
95
|
+
* not on string equality against a bare path.
|
|
96
|
+
*
|
|
97
|
+
* One route reaches a response without reaching this function: a
|
|
98
|
+
* `POST /v1/fine_tuning/jobs` whose body read throws (over the size cap, or a
|
|
99
|
+
* socket error) is caught in `server.ts` and answered 500 before
|
|
100
|
+
* `handleFineTuningCreate` runs, so it is never journaled. That is deliberate
|
|
101
|
+
* consistency, not an oversight — every sibling route's body-read catch in
|
|
102
|
+
* `server.ts` (responses, messages, cohere, moderation, embeddings, rerank,
|
|
103
|
+
* the video surfaces) writes the same unjournaled 500. Journaling it here alone
|
|
104
|
+
* would make fine-tuning the one surface whose journal disagrees with the rest.
|
|
105
|
+
*
|
|
106
|
+
* `source: "internal"` marks the entry as served by aimock's own synthetic job
|
|
107
|
+
* logic rather than a fixture or a proxy, which is the same value this file's
|
|
108
|
+
* own chaos gate passes for a faulted request. It is a rare value elsewhere:
|
|
109
|
+
* `grep -rn 'source: "internal"' src/*.ts` finds it on exactly two other
|
|
110
|
+
* writers, `images.ts`'s removed-endpoint 404 and `openrouter-video.ts`'s
|
|
111
|
+
* models listing synthesized after a failed proxy attempt — every other
|
|
112
|
+
* service's synthesized journal writes `source: "proxy"`. Fine-tuning is the
|
|
113
|
+
* one surface where every entry carries it, success and error alike, because
|
|
114
|
+
* no fixture or proxy ever serves this store.
|
|
115
|
+
*/
|
|
116
|
+
function journalFt(journal, method, path, headers, status, body = null) {
|
|
117
|
+
journal.add({
|
|
118
|
+
method,
|
|
119
|
+
path,
|
|
120
|
+
headers,
|
|
121
|
+
body,
|
|
122
|
+
service: "fine-tuning",
|
|
123
|
+
response: {
|
|
124
|
+
status,
|
|
125
|
+
fixture: null,
|
|
126
|
+
source: "internal"
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function writeJson(res, status, payload, setCorsHeaders) {
|
|
131
|
+
setCorsHeaders(res);
|
|
132
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
133
|
+
res.end(JSON.stringify(payload));
|
|
134
|
+
}
|
|
135
|
+
function invalid(message) {
|
|
136
|
+
return { error: {
|
|
137
|
+
message,
|
|
138
|
+
type: "invalid_request_error"
|
|
139
|
+
} };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Roll the chaos dice for one fine-tuning request. Returns true when a fault
|
|
143
|
+
* fired and the response is already written, so the caller returns early.
|
|
144
|
+
*
|
|
145
|
+
* CORS headers go on BEFORE the roll, because a fault writes the response
|
|
146
|
+
* itself and never reaches `writeJson` (the only other place this file sets
|
|
147
|
+
* them). Without this a browser client under chaos reads an opaque CORS
|
|
148
|
+
* failure instead of the 429/500/malformed body the chaos config asked for —
|
|
149
|
+
* exactly the fault it is trying to exercise, hidden. `setCorsHeaders` uses
|
|
150
|
+
* `res.setHeader`, so the later `writeHead` on the non-fault path merges with
|
|
151
|
+
* it rather than replacing it, and setting them twice is a no-op.
|
|
152
|
+
* `src/images.ts` orders it the same way (CORS at the top of the handler,
|
|
153
|
+
* chaos gate after).
|
|
154
|
+
*/
|
|
155
|
+
async function chaosHit(req, journal, defaults, method, path, res, setCorsHeaders) {
|
|
156
|
+
setCorsHeaders(res);
|
|
157
|
+
return await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
|
|
158
|
+
method,
|
|
159
|
+
path,
|
|
160
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
161
|
+
body: null,
|
|
162
|
+
service: "fine-tuning"
|
|
163
|
+
}, "internal", defaults.registry, defaults.logger);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Record one thing that just happened to `job`. Call sites append only at the
|
|
167
|
+
* moment of the transition they describe, so the log is the job's real
|
|
168
|
+
* history rather than a reconstruction from its current status.
|
|
169
|
+
*
|
|
170
|
+
* `created_at` comes from `stamp()`, so a log that reads out of order is not
|
|
171
|
+
* representable. Returns the second it stamped, which is what a terminal
|
|
172
|
+
* transition assigns to `finished_at` so that nothing can follow the finish.
|
|
173
|
+
*/
|
|
174
|
+
function appendEvent(job, message) {
|
|
175
|
+
const log = eventLogs.get(job.id) ?? [];
|
|
176
|
+
const createdAt = stamp();
|
|
177
|
+
log.push({
|
|
178
|
+
id: `ftevent-${job.id.replace(/^ftjob-/, "")}-${log.length}`,
|
|
179
|
+
object: "fine_tuning.job.event",
|
|
180
|
+
created_at: createdAt,
|
|
181
|
+
level: "info",
|
|
182
|
+
message,
|
|
183
|
+
type: "message"
|
|
184
|
+
});
|
|
185
|
+
eventLogs.set(job.id, log);
|
|
186
|
+
return createdAt;
|
|
187
|
+
}
|
|
188
|
+
function advance(job) {
|
|
189
|
+
if (isTerminal(job.status)) return job;
|
|
190
|
+
const n = (polls.get(job.id) ?? 0) + 1;
|
|
191
|
+
polls.set(job.id, n);
|
|
192
|
+
if (n === 1) {
|
|
193
|
+
job.status = "queued";
|
|
194
|
+
appendEvent(job, "Training file validated, job queued");
|
|
195
|
+
} else if (n === 2) {
|
|
196
|
+
job.status = "running";
|
|
197
|
+
appendEvent(job, "Training started");
|
|
198
|
+
} else {
|
|
199
|
+
job.status = "succeeded";
|
|
200
|
+
const suffix = suffixes.get(job.id);
|
|
201
|
+
const tail = suffix === void 0 ? "" : `${suffix}:`;
|
|
202
|
+
job.fine_tuned_model = `ft:${job.model}:aimock:${tail}${job.id.slice(-6)}`;
|
|
203
|
+
job.result_files = [`file-${job.id.replace(/^ftjob-/, "")}`];
|
|
204
|
+
job.trained_tokens = job.seed % 1e5;
|
|
205
|
+
job.finished_at = appendEvent(job, `Training complete, model ${job.fine_tuned_model}`);
|
|
206
|
+
}
|
|
207
|
+
return job;
|
|
208
|
+
}
|
|
209
|
+
function eventsFor(job) {
|
|
210
|
+
return [...eventLogs.get(job.id) ?? []];
|
|
211
|
+
}
|
|
212
|
+
/** `limit` default for both fine-tuning list endpoints (openai-openapi v2.3.0). */
|
|
213
|
+
const DEFAULT_PAGE_LIMIT = 20;
|
|
214
|
+
/** Upper bound we enforce on `limit`. Ours, not the vendor's — see `paginate`. */
|
|
215
|
+
const MAX_PAGE_LIMIT = 100;
|
|
216
|
+
/**
|
|
217
|
+
* The query parameters of a request target.
|
|
218
|
+
*
|
|
219
|
+
* The query is everything after the FIRST `?`, per RFC 3986 section 3.4
|
|
220
|
+
* ("The query component ... is terminated by a number sign (#) character or by
|
|
221
|
+
* the end of the URI") read together with RFC 9112 section 3.2: a request
|
|
222
|
+
* target is an origin-form `absolute-path [ "?" query ]` and carries no
|
|
223
|
+
* fragment component at all. Both halves of that matter here:
|
|
224
|
+
*
|
|
225
|
+
* - `split("?")[1]` truncates at a SECOND `?`, which is a legal literal
|
|
226
|
+
* inside a query, so `?a=1?b=2&limit=5` used to lose `limit` entirely.
|
|
227
|
+
* Slicing from the first `?` keeps it.
|
|
228
|
+
* - `new URL(target, base).searchParams` — what the router and the other
|
|
229
|
+
* query-reading mocks use — truncates at a `#` instead, because `URL`
|
|
230
|
+
* implements the fragment rule that a request target does not have. A
|
|
231
|
+
* caller that sends `?after=abc#def` on the wire means the cursor
|
|
232
|
+
* `abc#def`; reading it as `abc` is the same silent truncation one `?`
|
|
233
|
+
* along, and it turns a bad cursor into a plausible-looking one. An origin
|
|
234
|
+
* server sees no fragment, so neither does this.
|
|
235
|
+
*
|
|
236
|
+
* `new URLSearchParams(string)` cannot throw, so there is no parse failure to
|
|
237
|
+
* swallow and no defaults-fallback to hide one: every byte of the target after
|
|
238
|
+
* the first `?` is query data by definition. (A target malformed enough to
|
|
239
|
+
* break `new URL` never reaches this module — `src/server.ts` builds a `URL`
|
|
240
|
+
* to route on and fails there first.)
|
|
241
|
+
*/
|
|
242
|
+
function queryParams(url) {
|
|
243
|
+
const target = url ?? "/";
|
|
244
|
+
const start = target.indexOf("?");
|
|
245
|
+
return new URLSearchParams(start === -1 ? "" : target.slice(start + 1));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The RAW query text a parameter was sent with - what the caller typed, before
|
|
249
|
+
* percent- and `+`-decoding - or null when it was not sent at all.
|
|
250
|
+
*
|
|
251
|
+
* The unknown-`after` 400 below quotes the offending cursor, and the docs
|
|
252
|
+
* promise it quotes "what you sent". `URLSearchParams` hands back the DECODED
|
|
253
|
+
* value, so `?after=a%2Bb` was refused as `'a+b'` and `?after=a+b` as `'a b'` -
|
|
254
|
+
* neither of which appears anywhere in the request the caller is staring at,
|
|
255
|
+
* which is the one thing an error quoting a value is for.
|
|
256
|
+
*
|
|
257
|
+
* Pair splitting mirrors `URLSearchParams` itself, which splits on `&` alone
|
|
258
|
+
* (`;` has not been a separator since the URL spec absorbed it), and the NAME
|
|
259
|
+
* is matched through the platform's own decoder - one `URLSearchParams` per
|
|
260
|
+
* pair - so an encoded spelling of the name still matches and nothing here
|
|
261
|
+
* hand-rolls the percent codec. Only the VALUE is kept as raw text.
|
|
262
|
+
*/
|
|
263
|
+
function rawParam(url, name) {
|
|
264
|
+
const target = url ?? "/";
|
|
265
|
+
const start = target.indexOf("?");
|
|
266
|
+
if (start === -1) return null;
|
|
267
|
+
for (const pair of target.slice(start + 1).split("&")) {
|
|
268
|
+
if (pair.length === 0) continue;
|
|
269
|
+
const [decodedName] = new URLSearchParams(pair).keys();
|
|
270
|
+
if (decodedName !== name) continue;
|
|
271
|
+
const eq = pair.indexOf("=");
|
|
272
|
+
return eq === -1 ? "" : pair.slice(eq + 1);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The single value of a query parameter, or the 400 a repeated one earns.
|
|
278
|
+
*
|
|
279
|
+
* `URLSearchParams.get` answers with the FIRST of a repeated parameter, which
|
|
280
|
+
* launders the rest: `?limit=1&limit=abc` used to page at 1 and never look at
|
|
281
|
+
* `abc`, so a caller building a URL twice over got a silent success where the
|
|
282
|
+
* value it actually meant was invalid. The spec declares each of `limit` and
|
|
283
|
+
* `after` as a single scalar (`listPaginatedFineTuningJobs`,
|
|
284
|
+
* `listFineTuningEvents`) and gives no combining rule for a repeat, so a repeat
|
|
285
|
+
* is a caller error here. That the 400 (rather than last-wins, or first-wins)
|
|
286
|
+
* is ours, not the vendor's, is recorded in this module's header.
|
|
287
|
+
*/
|
|
288
|
+
function singleParam(params, name) {
|
|
289
|
+
const all = params.getAll(name);
|
|
290
|
+
if (all.length > 1) return {
|
|
291
|
+
ok: false,
|
|
292
|
+
message: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`
|
|
293
|
+
};
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
value: all.length === 0 ? null : all[0]
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Cursor page over an array already ordered newest-first, mirroring the
|
|
301
|
+
* `after`/`limit` params both fine-tuning list endpoints accept. `has_more`
|
|
302
|
+
* reports whether anything was left behind, so it is never a constant.
|
|
303
|
+
*
|
|
304
|
+
* Sourced 2026-09-15 from openai/openai-openapi `openapi.yaml` v2.3.0
|
|
305
|
+
* (`listPaginatedFineTuningJobs`, `listFineTuningEvents`) and the vendored
|
|
306
|
+
* `openai` SDK 4.104.0 `pagination.d.ts`/`pagination.js` `CursorPage`:
|
|
307
|
+
* - `after` is a free-form string id; `CursorPage.nextPageInfo()` feeds the
|
|
308
|
+
* LAST id of the page back as `after`, and `hasNextPage()` stops on
|
|
309
|
+
* `has_more: false`. Confirmed by the spec.
|
|
310
|
+
* - `limit` is `type: integer, default: 20` on both endpoints. Confirmed by
|
|
311
|
+
* the spec; the default is the vendor's.
|
|
312
|
+
*
|
|
313
|
+
* Three rules the spec does NOT state, chosen here and flagged as ours:
|
|
314
|
+
* - Upper bound 100 on `limit`. The spec declares no `maximum`. We enforce
|
|
315
|
+
* one so an oversized page fails loudly instead of being silently clamped,
|
|
316
|
+
* and 100 is the number our own docs page already advertises.
|
|
317
|
+
* - An `after` that matches no item is a 400 rather than a silent page 1.
|
|
318
|
+
* Returning page 1 makes the SDK's `CursorPage` auto-paginator restart from
|
|
319
|
+
* the top forever on a stale cursor; failing loudly is the whole point of a
|
|
320
|
+
* mock. Not observed against api.openai.com.
|
|
321
|
+
* - Newest-first ordering. Neither list endpoint accepts an `order` param and
|
|
322
|
+
* the spec never states a direction; the `listFineTuningEvents` example
|
|
323
|
+
* response lists the completion event ahead of the earlier
|
|
324
|
+
* model-created event, which reads as reverse-chronological.
|
|
325
|
+
*/
|
|
326
|
+
function paginate(items, url) {
|
|
327
|
+
const params = queryParams(url);
|
|
328
|
+
const readLimit = singleParam(params, "limit");
|
|
329
|
+
if (!readLimit.ok) return {
|
|
330
|
+
ok: false,
|
|
331
|
+
message: readLimit.message
|
|
332
|
+
};
|
|
333
|
+
const rawLimit = readLimit.value;
|
|
334
|
+
let limit = DEFAULT_PAGE_LIMIT;
|
|
335
|
+
if (rawLimit !== null) {
|
|
336
|
+
const n = require_helpers.parseStrictIntegerText(rawLimit);
|
|
337
|
+
if (n === null || n < 1 || n > MAX_PAGE_LIMIT) return {
|
|
338
|
+
ok: false,
|
|
339
|
+
message: `Invalid parameter: 'limit' must be an integer between 1 and ${MAX_PAGE_LIMIT}, got '${rawParam(url, "limit") ?? rawLimit}'`
|
|
340
|
+
};
|
|
341
|
+
limit = n;
|
|
342
|
+
}
|
|
343
|
+
const readAfter = singleParam(params, "after");
|
|
344
|
+
if (!readAfter.ok) return {
|
|
345
|
+
ok: false,
|
|
346
|
+
message: readAfter.message
|
|
347
|
+
};
|
|
348
|
+
const after = readAfter.value;
|
|
349
|
+
let start = 0;
|
|
350
|
+
if (after !== null) {
|
|
351
|
+
const idx = items.findIndex((it) => it.id === after);
|
|
352
|
+
if (idx === -1) return {
|
|
353
|
+
ok: false,
|
|
354
|
+
message: `Invalid parameter: 'after' is not a known cursor: '${rawParam(url, "after") ?? after}'`
|
|
355
|
+
};
|
|
356
|
+
start = idx + 1;
|
|
357
|
+
}
|
|
358
|
+
const rest = items.slice(start);
|
|
359
|
+
return {
|
|
360
|
+
ok: true,
|
|
361
|
+
page: {
|
|
362
|
+
object: "list",
|
|
363
|
+
data: rest.slice(0, limit),
|
|
364
|
+
has_more: rest.length > limit
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** Write a paginated list, or the 400 its `after`/`limit` params earned. */
|
|
369
|
+
function writePage(res, journal, req, method, path, items, setCorsHeaders) {
|
|
370
|
+
const result = paginate(items, req.url);
|
|
371
|
+
const status = result.ok ? 200 : 400;
|
|
372
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), status);
|
|
373
|
+
writeJson(res, status, result.ok ? result.page : invalid(result.message), setCorsHeaders);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Deterministic seed in the spec's `[0, 2147483647]` range, derived from the
|
|
377
|
+
* job id so repeated reads of the same job report the same seed.
|
|
378
|
+
*/
|
|
379
|
+
function seedFor(id) {
|
|
380
|
+
let h = 2166136261;
|
|
381
|
+
for (let i = 0; i < id.length; i++) h = Math.imul(h ^ id.charCodeAt(i), 16777619);
|
|
382
|
+
return (h >>> 0) % 2147483648;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The three members `CreateFineTuningJobRequest.hyperparameters` and
|
|
386
|
+
* `FineTuneSupervisedHyperparameters` both declare, quoted from
|
|
387
|
+
* openai/openai-openapi `openapi.yaml` v2.3.0 (`master`):
|
|
388
|
+
* - `n_epochs`: `type: integer, minimum: 1, maximum: 50`
|
|
389
|
+
* - `batch_size`: `type: integer, minimum: 1, maximum: 256`
|
|
390
|
+
* - `learning_rate_multiplier`: `type: number, minimum: 0,
|
|
391
|
+
* exclusiveMinimum: true` — strictly greater than zero, hence no `max`.
|
|
392
|
+
*
|
|
393
|
+
* The vendored SDK's `JobCreateParams.Hyperparameters` types each member as
|
|
394
|
+
* `"auto" | number`, which is as much as TypeScript can say; the bounds only
|
|
395
|
+
* exist in the spec, so they are enforced here from it.
|
|
396
|
+
*/
|
|
397
|
+
const SUPERVISED_HYPER_RULES = {
|
|
398
|
+
n_epochs: {
|
|
399
|
+
kind: "integer",
|
|
400
|
+
min: 1,
|
|
401
|
+
max: 50
|
|
402
|
+
},
|
|
403
|
+
batch_size: {
|
|
404
|
+
kind: "integer",
|
|
405
|
+
min: 1,
|
|
406
|
+
max: 256
|
|
407
|
+
},
|
|
408
|
+
learning_rate_multiplier: {
|
|
409
|
+
kind: "number",
|
|
410
|
+
exclusiveMin: 0,
|
|
411
|
+
max: null
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* The deprecated top-level `hyperparameters` object. The spec declares exactly
|
|
416
|
+
* the three supervised members on it — `beta` and the reinforcement knobs live
|
|
417
|
+
* only under `method`, and the description says this whole object "is now
|
|
418
|
+
* deprecated in favor of `method`".
|
|
419
|
+
*/
|
|
420
|
+
const LEGACY_HYPER_RULES = SUPERVISED_HYPER_RULES;
|
|
421
|
+
/**
|
|
422
|
+
* `FineTuneDPOHyperparameters`: the supervised three plus `beta`, declared
|
|
423
|
+
* `type: number, minimum: 0, exclusiveMinimum: true, maximum: 2`.
|
|
424
|
+
*/
|
|
425
|
+
const DPO_HYPER_RULES = {
|
|
426
|
+
...SUPERVISED_HYPER_RULES,
|
|
427
|
+
beta: {
|
|
428
|
+
kind: "number",
|
|
429
|
+
exclusiveMin: 0,
|
|
430
|
+
max: 2
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* `FineTuneReinforcementHyperparameters`: the supervised three plus
|
|
435
|
+
* `reasoning_effort` (`enum: [default, low, medium, high]`, no `"auto"`),
|
|
436
|
+
* `compute_multiplier` (`minimum: 0.00001, exclusiveMinimum: true,
|
|
437
|
+
* maximum: 10`), and `eval_interval`/`eval_samples` (`type: integer,
|
|
438
|
+
* minimum: 1`, no declared maximum).
|
|
439
|
+
*/
|
|
440
|
+
const REINFORCEMENT_HYPER_RULES = {
|
|
441
|
+
...SUPERVISED_HYPER_RULES,
|
|
442
|
+
reasoning_effort: {
|
|
443
|
+
kind: "enum",
|
|
444
|
+
values: [
|
|
445
|
+
"default",
|
|
446
|
+
"low",
|
|
447
|
+
"medium",
|
|
448
|
+
"high"
|
|
449
|
+
]
|
|
450
|
+
},
|
|
451
|
+
compute_multiplier: {
|
|
452
|
+
kind: "number",
|
|
453
|
+
exclusiveMin: 1e-5,
|
|
454
|
+
max: 10
|
|
455
|
+
},
|
|
456
|
+
eval_interval: {
|
|
457
|
+
kind: "integer",
|
|
458
|
+
min: 1,
|
|
459
|
+
max: null
|
|
460
|
+
},
|
|
461
|
+
eval_samples: {
|
|
462
|
+
kind: "integer",
|
|
463
|
+
min: 1,
|
|
464
|
+
max: null
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
/** How a rule reads in an error message, so the caller learns the domain. */
|
|
468
|
+
function describeRule(rule) {
|
|
469
|
+
if (rule.kind === "enum") return `one of ${rule.values.map((v) => `"${v}"`).join(", ")}`;
|
|
470
|
+
const upper = rule.max === null ? "" : ` and at most ${rule.max}`;
|
|
471
|
+
return `${rule.kind === "integer" ? `an integer at least ${rule.min}` : `a number greater than ${rule.exclusiveMin}`}${upper}, or the string "auto"`;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Reads one hyperparameter against its rule. Anything outside the rule is a
|
|
475
|
+
* caller error, never a silent drop — a mock that accepted `n_epochs: 0` would
|
|
476
|
+
* let a bug reach the real API instead of failing where it is cheap to see.
|
|
477
|
+
*
|
|
478
|
+
* `path` names the object the member came from (`hyperparameters`,
|
|
479
|
+
* `method.dpo.hyperparameters`, …) so the message points at the thing the
|
|
480
|
+
* caller actually wrote.
|
|
481
|
+
*/
|
|
482
|
+
function readHyper(table, path, key, value) {
|
|
483
|
+
const rule = Object.hasOwn(table, key) ? table[key] : void 0;
|
|
484
|
+
if (rule === void 0) return {
|
|
485
|
+
ok: false,
|
|
486
|
+
message: unknownMemberMessage(table, path, key)
|
|
487
|
+
};
|
|
488
|
+
const bad = {
|
|
489
|
+
ok: false,
|
|
490
|
+
message: `Invalid parameter: '${path}.${key}' must be ${describeRule(rule)}`
|
|
491
|
+
};
|
|
492
|
+
if (rule.kind === "enum") return typeof value === "string" && rule.values.includes(value) ? {
|
|
493
|
+
ok: true,
|
|
494
|
+
value
|
|
495
|
+
} : bad;
|
|
496
|
+
if (value === "auto") return {
|
|
497
|
+
ok: true,
|
|
498
|
+
value: "auto"
|
|
499
|
+
};
|
|
500
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return bad;
|
|
501
|
+
if (rule.max !== null && value > rule.max) return bad;
|
|
502
|
+
if (rule.kind === "integer") return value >= rule.min && Number.isInteger(value) ? {
|
|
503
|
+
ok: true,
|
|
504
|
+
value
|
|
505
|
+
} : bad;
|
|
506
|
+
return value > rule.exclusiveMin ? {
|
|
507
|
+
ok: true,
|
|
508
|
+
value
|
|
509
|
+
} : bad;
|
|
510
|
+
}
|
|
511
|
+
/** The 400 an unrecognised member of a rule-table-backed object earns. */
|
|
512
|
+
function unknownMemberMessage(table, path, key) {
|
|
513
|
+
return `Invalid parameter: '${path}.${key}' is not a recognized member of '${path}'; it accepts ${Object.keys(table).join(", ")}`;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Reads a whole hyperparameters object against its rule table.
|
|
517
|
+
*
|
|
518
|
+
* An unrecognised member is a 400 rather than a silent drop. The spec does not
|
|
519
|
+
* mark these objects `additionalProperties: false`, so this rule is ours, not
|
|
520
|
+
* the vendor's — and it is the point of the surface: `{n_epochs: 3, beta: 0.5}`
|
|
521
|
+
* used to answer 200 echoing only `n_epochs`, so a caller who put a real DPO
|
|
522
|
+
* hyperparameter (`FineTuneDPOHyperparameters.beta`) in the deprecated
|
|
523
|
+
* top-level object instead of under `method.dpo.hyperparameters` was told it
|
|
524
|
+
* had been accepted. Naming the members the object does take is what turns
|
|
525
|
+
* that into a fixable message.
|
|
526
|
+
*/
|
|
527
|
+
function readHyperObject(table, path, raw) {
|
|
528
|
+
const out = {};
|
|
529
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
530
|
+
if (value === void 0) continue;
|
|
531
|
+
if (!Object.hasOwn(table, key)) return {
|
|
532
|
+
ok: false,
|
|
533
|
+
message: unknownMemberMessage(table, path, key)
|
|
534
|
+
};
|
|
535
|
+
const read = readHyper(table, path, key, value);
|
|
536
|
+
if (!read.ok) return read;
|
|
537
|
+
out[key] = read.value;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
value: out
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Every property `CreateFineTuningJobRequest` declares (openai-openapi v2.3.0).
|
|
546
|
+
*
|
|
547
|
+
* Each one is read below; nothing on this list is accepted and dropped, and
|
|
548
|
+
* anything NOT on it is a 400. The spec does not mark the request object
|
|
549
|
+
* `additionalProperties: false`, so rejecting an unknown key is this mock's
|
|
550
|
+
* rule, not the vendor's: a create body is written once and reused for months,
|
|
551
|
+
* and a typo that answers 200 having ignored the field is exactly the bug a
|
|
552
|
+
* mock exists to catch.
|
|
553
|
+
*/
|
|
554
|
+
const CREATE_KEYS = [
|
|
555
|
+
"model",
|
|
556
|
+
"training_file",
|
|
557
|
+
"hyperparameters",
|
|
558
|
+
"suffix",
|
|
559
|
+
"validation_file",
|
|
560
|
+
"integrations",
|
|
561
|
+
"seed",
|
|
562
|
+
"method",
|
|
563
|
+
"metadata"
|
|
564
|
+
];
|
|
565
|
+
/** The members of the deprecated top-level `hyperparameters`, as the job echoes them. */
|
|
566
|
+
const LEGACY_HYPER_KEYS = [
|
|
567
|
+
"n_epochs",
|
|
568
|
+
"batch_size",
|
|
569
|
+
"learning_rate_multiplier"
|
|
570
|
+
];
|
|
571
|
+
/** `Metadata` in the spec: at most 16 pairs, keys <= 64 chars, values <= 512. */
|
|
572
|
+
const METADATA_MAX_PAIRS = 16;
|
|
573
|
+
const METADATA_MAX_KEY = 64;
|
|
574
|
+
const METADATA_MAX_VALUE = 512;
|
|
575
|
+
/** `CreateFineTuningJobRequest.suffix`: `minLength: 1, maxLength: 64`. */
|
|
576
|
+
const SUFFIX_MAX_LENGTH = 64;
|
|
577
|
+
/** `CreateFineTuningJobRequest.seed`: `minimum: 0, maximum: 2147483647`. */
|
|
578
|
+
const SEED_MAX = 2147483647;
|
|
579
|
+
/** `FineTuningJob.integrations`: `maxItems: 5`. */
|
|
580
|
+
const MAX_INTEGRATIONS = 5;
|
|
581
|
+
/**
|
|
582
|
+
* Length in Unicode CODE POINTS, which is the unit the spec's limits are in.
|
|
583
|
+
*
|
|
584
|
+
* Every `maxLength` quoted in this module is a pydantic `max_length` on the
|
|
585
|
+
* vendor's side, and pydantic counts code points; `String.prototype.length`
|
|
586
|
+
* counts UTF-16 code units, which is the same number only while the string
|
|
587
|
+
* stays inside the BMP. One astral character - an emoji, a rarer CJK
|
|
588
|
+
* ideograph, a musical symbol - counts two there, so a `suffix` of exactly 64
|
|
589
|
+
* characters holding one used to earn a 400 the real API would not send, and a
|
|
590
|
+
* caller that trusted the mock would have shortened a name that was legal.
|
|
591
|
+
*/
|
|
592
|
+
function codePointLength(value) {
|
|
593
|
+
return [...value].length;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* `suffix`, per its spec description: "A string of up to 64 characters that
|
|
597
|
+
* will be added to your fine-tuned model name. For example, a `suffix` of
|
|
598
|
+
* "custom-model-name" would produce a model name like
|
|
599
|
+
* `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`." It is declared
|
|
600
|
+
* `type: string, minLength: 1, maxLength: 64, nullable: true, default: null`,
|
|
601
|
+
* so `null` means "no suffix" and `""` is a 400.
|
|
602
|
+
*/
|
|
603
|
+
function readSuffix(value) {
|
|
604
|
+
if (value === void 0 || value === null) return {
|
|
605
|
+
ok: true,
|
|
606
|
+
value: null
|
|
607
|
+
};
|
|
608
|
+
if (typeof value !== "string" || value.length === 0 || codePointLength(value) > SUFFIX_MAX_LENGTH) return {
|
|
609
|
+
ok: false,
|
|
610
|
+
message: `Invalid parameter: 'suffix' must be a string of 1 to ${SUFFIX_MAX_LENGTH} characters or null`
|
|
611
|
+
};
|
|
612
|
+
return {
|
|
613
|
+
ok: true,
|
|
614
|
+
value
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* `seed`, per its spec description: "The seed controls the reproducibility of
|
|
619
|
+
* the job ... If a seed is not specified, one will be generated for you", and
|
|
620
|
+
* declared `type: integer, nullable: true, minimum: 0, maximum: 2147483647`.
|
|
621
|
+
* `null` and absence both mean "generate one", which is what `seedFor` does.
|
|
622
|
+
*/
|
|
623
|
+
function readSeed(value) {
|
|
624
|
+
if (value === void 0 || value === null) return {
|
|
625
|
+
ok: true,
|
|
626
|
+
value: null
|
|
627
|
+
};
|
|
628
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > SEED_MAX) return {
|
|
629
|
+
ok: false,
|
|
630
|
+
message: `Invalid parameter: 'seed' must be an integer between 0 and ${SEED_MAX} or null`
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
ok: true,
|
|
634
|
+
value
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* `metadata`, per the shared `Metadata` schema: an object of string values, at
|
|
639
|
+
* most 16 pairs, keys at most 64 characters and values at most 512, or `null`.
|
|
640
|
+
* `FineTuningJob` declares `metadata` too, so what is accepted is echoed back.
|
|
641
|
+
*/
|
|
642
|
+
function readMetadata(value) {
|
|
643
|
+
if (value === void 0 || value === null) return {
|
|
644
|
+
ok: true,
|
|
645
|
+
value: null
|
|
646
|
+
};
|
|
647
|
+
if (!require_helpers.isJsonObject(value)) return {
|
|
648
|
+
ok: false,
|
|
649
|
+
message: "Invalid parameter: 'metadata' must be a JSON object or null"
|
|
650
|
+
};
|
|
651
|
+
const entries = Object.entries(value);
|
|
652
|
+
if (entries.length > METADATA_MAX_PAIRS) return {
|
|
653
|
+
ok: false,
|
|
654
|
+
message: `Invalid parameter: 'metadata' accepts at most ${METADATA_MAX_PAIRS} key-value pairs, got ${entries.length}`
|
|
655
|
+
};
|
|
656
|
+
const out = Object.create(null);
|
|
657
|
+
for (const [key, entry] of entries) {
|
|
658
|
+
const keyLength = codePointLength(key);
|
|
659
|
+
if (keyLength > METADATA_MAX_KEY) return {
|
|
660
|
+
ok: false,
|
|
661
|
+
message: `Invalid parameter: 'metadata' keys are at most ${METADATA_MAX_KEY} characters, got ${keyLength}`
|
|
662
|
+
};
|
|
663
|
+
if (typeof entry !== "string" || codePointLength(entry) > METADATA_MAX_VALUE) return {
|
|
664
|
+
ok: false,
|
|
665
|
+
message: `Invalid parameter: 'metadata.${key}' must be a string of at most ${METADATA_MAX_VALUE} characters`
|
|
666
|
+
};
|
|
667
|
+
out[key] = entry;
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
ok: true,
|
|
671
|
+
value: out
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* One `integrations` entry. The spec requires both `type` and `wandb`, types
|
|
676
|
+
* `type` as the single-member enum `wandb`, and requires `project` inside
|
|
677
|
+
* `wandb`; `name` and `entity` are nullable strings and `tags` an array of
|
|
678
|
+
* strings. `FineTuningJob.integrations` caps the list at `maxItems: 5` — the
|
|
679
|
+
* request schema declares no cap, so enforcing the response's here is ours.
|
|
680
|
+
*/
|
|
681
|
+
function readIntegration(index, raw) {
|
|
682
|
+
const at = `integrations[${index}]`;
|
|
683
|
+
if (!require_helpers.isJsonObject(raw)) return {
|
|
684
|
+
ok: false,
|
|
685
|
+
message: `Invalid parameter: '${at}' must be a JSON object`
|
|
686
|
+
};
|
|
687
|
+
for (const key of Object.keys(raw)) if (key !== "type" && key !== "wandb") return {
|
|
688
|
+
ok: false,
|
|
689
|
+
message: `Invalid parameter: '${at}.${key}' is not a recognized member of '${at}'; it accepts type, wandb`
|
|
690
|
+
};
|
|
691
|
+
if (raw["type"] !== "wandb") return {
|
|
692
|
+
ok: false,
|
|
693
|
+
message: `Invalid parameter: '${at}.type' must be the string "wandb"`
|
|
694
|
+
};
|
|
695
|
+
const wandb = raw["wandb"];
|
|
696
|
+
if (!require_helpers.isJsonObject(wandb)) return {
|
|
697
|
+
ok: false,
|
|
698
|
+
message: `Invalid parameter: '${at}.wandb' must be a JSON object`
|
|
699
|
+
};
|
|
700
|
+
for (const key of Object.keys(wandb)) if (key !== "project" && key !== "name" && key !== "entity" && key !== "tags") return {
|
|
701
|
+
ok: false,
|
|
702
|
+
message: `Invalid parameter: '${at}.wandb.${key}' is not a recognized member of '${at}.wandb'; it accepts project, name, entity, tags`
|
|
703
|
+
};
|
|
704
|
+
const project = wandb["project"];
|
|
705
|
+
if (typeof project !== "string" || project.length === 0) return {
|
|
706
|
+
ok: false,
|
|
707
|
+
message: `Invalid parameter: '${at}.wandb.project' must be a non-empty string`
|
|
708
|
+
};
|
|
709
|
+
const value = {
|
|
710
|
+
type: "wandb",
|
|
711
|
+
wandb: { project }
|
|
712
|
+
};
|
|
713
|
+
for (const key of ["name", "entity"]) {
|
|
714
|
+
const member = wandb[key];
|
|
715
|
+
if (member === void 0) continue;
|
|
716
|
+
if (member !== null && typeof member !== "string") return {
|
|
717
|
+
ok: false,
|
|
718
|
+
message: `Invalid parameter: '${at}.wandb.${key}' must be a string or null`
|
|
719
|
+
};
|
|
720
|
+
value.wandb[key] = member;
|
|
721
|
+
}
|
|
722
|
+
const tags = wandb["tags"];
|
|
723
|
+
if (tags !== void 0) {
|
|
724
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) return {
|
|
725
|
+
ok: false,
|
|
726
|
+
message: `Invalid parameter: '${at}.wandb.tags' must be an array of strings`
|
|
727
|
+
};
|
|
728
|
+
value.wandb.tags = tags.filter((tag) => typeof tag === "string");
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
ok: true,
|
|
732
|
+
value
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/** `integrations`: a nullable array of the entries above. */
|
|
736
|
+
function readIntegrations(value) {
|
|
737
|
+
if (value === void 0 || value === null) return {
|
|
738
|
+
ok: true,
|
|
739
|
+
value: null
|
|
740
|
+
};
|
|
741
|
+
if (!Array.isArray(value)) return {
|
|
742
|
+
ok: false,
|
|
743
|
+
message: "Invalid parameter: 'integrations' must be an array or null"
|
|
744
|
+
};
|
|
745
|
+
if (value.length > MAX_INTEGRATIONS) return {
|
|
746
|
+
ok: false,
|
|
747
|
+
message: `Invalid parameter: 'integrations' accepts at most ${MAX_INTEGRATIONS} entries, got ${value.length}`
|
|
748
|
+
};
|
|
749
|
+
const out = [];
|
|
750
|
+
for (const [index, entry] of value.entries()) {
|
|
751
|
+
const read = readIntegration(index, entry);
|
|
752
|
+
if (!read.ok) return read;
|
|
753
|
+
out.push(read.value);
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
ok: true,
|
|
757
|
+
value: out
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/** The rule table each `method` variant's `hyperparameters` is read against. */
|
|
761
|
+
const METHOD_HYPER_RULES = {
|
|
762
|
+
supervised: SUPERVISED_HYPER_RULES,
|
|
763
|
+
dpo: DPO_HYPER_RULES,
|
|
764
|
+
reinforcement: REINFORCEMENT_HYPER_RULES
|
|
765
|
+
};
|
|
766
|
+
const METHOD_TYPES = [
|
|
767
|
+
"supervised",
|
|
768
|
+
"dpo",
|
|
769
|
+
"reinforcement"
|
|
770
|
+
];
|
|
771
|
+
/**
|
|
772
|
+
* `method`, per `FineTuneMethod`: `type` is required and is one of
|
|
773
|
+
* `supervised`, `dpo`, `reinforcement`, and each of those names an optional
|
|
774
|
+
* sub-object carrying that method's `hyperparameters`. `FineTuneReinforcementMethod`
|
|
775
|
+
* additionally requires a `grader`, so `type: "reinforcement"` requires one
|
|
776
|
+
* whether or not the `reinforcement` sub-object is present at all — the
|
|
777
|
+
* requirement is keyed on the chosen `type`, not on the sub-object happening
|
|
778
|
+
* to be there.
|
|
779
|
+
*
|
|
780
|
+
* Only the chosen type's sub-object is accepted. `FineTuneMethod` leaves all
|
|
781
|
+
* three optional and says nothing about a configuration for a type that was
|
|
782
|
+
* not chosen, and neither does SDK 4.104.0's `JobCreateParams.Method`; every
|
|
783
|
+
* example in the spec pairs a `type` with that type's own configuration and
|
|
784
|
+
* no other (`createFineTuningJob`, the "Reinforcement" example). With the real
|
|
785
|
+
* API's behaviour uncitable, a stray configuration is rejected rather than
|
|
786
|
+
* accepted-and-ignored: this mock never takes a member it then drops.
|
|
787
|
+
*
|
|
788
|
+
* The grader itself (`GraderStringCheck` | `GraderTextSimilarity` |
|
|
789
|
+
* `GraderPython` | `GraderScoreModel` | `GraderMulti`) is a schema tree of its
|
|
790
|
+
* own that this mock does not model: it is required to be a JSON object and is
|
|
791
|
+
* then echoed back verbatim on the job. That is a deliberate pass-through, not
|
|
792
|
+
* a drop — what the caller sent is what the caller reads back.
|
|
793
|
+
*
|
|
794
|
+
* `FineTuningJob` declares `method`, so an accepted method is echoed.
|
|
795
|
+
*/
|
|
796
|
+
function readMethod(value) {
|
|
797
|
+
if (value === void 0 || value === null) return {
|
|
798
|
+
ok: true,
|
|
799
|
+
value: null
|
|
800
|
+
};
|
|
801
|
+
if (!require_helpers.isJsonObject(value)) return {
|
|
802
|
+
ok: false,
|
|
803
|
+
message: "Invalid parameter: 'method' must be a JSON object or null"
|
|
804
|
+
};
|
|
805
|
+
for (const key of Object.keys(value)) if (key !== "type" && !METHOD_TYPES.some((t) => t === key)) return {
|
|
806
|
+
ok: false,
|
|
807
|
+
message: `Invalid parameter: 'method.${key}' is not a recognized member of 'method'; it accepts type, ${METHOD_TYPES.join(", ")}`
|
|
808
|
+
};
|
|
809
|
+
const type = METHOD_TYPES.find((t) => t === value["type"]);
|
|
810
|
+
if (type === void 0) return {
|
|
811
|
+
ok: false,
|
|
812
|
+
message: `Invalid parameter: 'method.type' must be one of ${METHOD_TYPES.map((t) => `"${t}"`).join(", ")}`
|
|
813
|
+
};
|
|
814
|
+
const method = { type };
|
|
815
|
+
for (const variant of METHOD_TYPES) if (variant !== type && value[variant] !== void 0) return {
|
|
816
|
+
ok: false,
|
|
817
|
+
message: `Invalid parameter: 'method.${variant}' configures the "${variant}" method, but 'method.type' is "${type}"; a method carries only its own configuration`
|
|
818
|
+
};
|
|
819
|
+
const supplied = value[type];
|
|
820
|
+
const raw = supplied === void 0 && type === "reinforcement" ? {} : supplied;
|
|
821
|
+
if (raw !== void 0) {
|
|
822
|
+
if (!require_helpers.isJsonObject(raw)) return {
|
|
823
|
+
ok: false,
|
|
824
|
+
message: `Invalid parameter: 'method.${type}' must be a JSON object`
|
|
825
|
+
};
|
|
826
|
+
const allowed = type === "reinforcement" ? ["grader", "hyperparameters"] : ["hyperparameters"];
|
|
827
|
+
for (const key of Object.keys(raw)) if (!allowed.includes(key)) return {
|
|
828
|
+
ok: false,
|
|
829
|
+
message: `Invalid parameter: 'method.${type}.${key}' is not a recognized member of 'method.${type}'; it accepts ${allowed.join(", ")}`
|
|
830
|
+
};
|
|
831
|
+
const config = {};
|
|
832
|
+
if (type === "reinforcement") {
|
|
833
|
+
const grader = raw["grader"];
|
|
834
|
+
if (!require_helpers.isJsonObject(grader)) return {
|
|
835
|
+
ok: false,
|
|
836
|
+
message: "Invalid parameter: 'method.reinforcement.grader' is required and must be a JSON object"
|
|
837
|
+
};
|
|
838
|
+
config.grader = grader;
|
|
839
|
+
}
|
|
840
|
+
const hyper = raw["hyperparameters"];
|
|
841
|
+
if (hyper !== void 0) {
|
|
842
|
+
if (!require_helpers.isJsonObject(hyper)) return {
|
|
843
|
+
ok: false,
|
|
844
|
+
message: `Invalid parameter: 'method.${type}.hyperparameters' must be a JSON object`
|
|
845
|
+
};
|
|
846
|
+
const read = readHyperObject(METHOD_HYPER_RULES[type], `method.${type}.hyperparameters`, hyper);
|
|
847
|
+
if (!read.ok) return read;
|
|
848
|
+
config.hyperparameters = read.value;
|
|
849
|
+
}
|
|
850
|
+
method[type] = config;
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
ok: true,
|
|
854
|
+
value: method
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
async function handleFineTuningCreate(req, res, raw, journal, defaults, setCorsHeaders) {
|
|
858
|
+
const path = req.url ?? "/v1/fine_tuning/jobs";
|
|
859
|
+
const method = req.method ?? "POST";
|
|
860
|
+
if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
|
|
861
|
+
let body;
|
|
862
|
+
try {
|
|
863
|
+
body = JSON.parse(raw);
|
|
864
|
+
} catch (err) {
|
|
865
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 400);
|
|
866
|
+
writeJson(res, 400, invalid(`Malformed JSON: ${err instanceof Error ? err.message : "unknown"}`), setCorsHeaders);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (!require_helpers.isJsonObject(body)) {
|
|
870
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 400);
|
|
871
|
+
writeJson(res, 400, invalid("Request body must be a JSON object"), setCorsHeaders);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const journaledBody = body;
|
|
875
|
+
/**
|
|
876
|
+
* Every rejection below journals the parsed body before answering: these are
|
|
877
|
+
* exactly the 400s whose offending payload a caller needs to read back.
|
|
878
|
+
*/
|
|
879
|
+
const reject = (message) => {
|
|
880
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 400, journaledBody);
|
|
881
|
+
writeJson(res, 400, invalid(message), setCorsHeaders);
|
|
882
|
+
};
|
|
883
|
+
for (const key of Object.keys(body)) if (!CREATE_KEYS.some((known) => known === key)) {
|
|
884
|
+
reject(`Invalid parameter: '${key}' is not a recognized request parameter; this endpoint accepts ${CREATE_KEYS.join(", ")}`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const trainingFile = body["training_file"];
|
|
888
|
+
const model = body["model"];
|
|
889
|
+
if (typeof trainingFile !== "string" || trainingFile.length === 0) {
|
|
890
|
+
reject("Invalid parameter: 'training_file' must be a non-empty string");
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
894
|
+
reject("Invalid parameter: 'model' must be a non-empty string");
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const validationFile = body["validation_file"];
|
|
898
|
+
if (validationFile !== void 0 && validationFile !== null && (typeof validationFile !== "string" || validationFile.length === 0)) {
|
|
899
|
+
reject("Invalid parameter: 'validation_file' must be a non-empty string or null");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const hyper = body["hyperparameters"];
|
|
903
|
+
if (hyper !== void 0 && !require_helpers.isJsonObject(hyper)) {
|
|
904
|
+
reject("Invalid parameter: 'hyperparameters' must be a JSON object");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const hyperparameters = {};
|
|
908
|
+
if (require_helpers.isJsonObject(hyper)) {
|
|
909
|
+
const read = readHyperObject(LEGACY_HYPER_RULES, "hyperparameters", hyper);
|
|
910
|
+
if (!read.ok) {
|
|
911
|
+
reject(read.message);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
for (const key of LEGACY_HYPER_KEYS) {
|
|
915
|
+
const value = read.value[key];
|
|
916
|
+
if (typeof value === "number" || value === "auto") hyperparameters[key] = value;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const suffix = readSuffix(body["suffix"]);
|
|
920
|
+
if (!suffix.ok) {
|
|
921
|
+
reject(suffix.message);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const seed = readSeed(body["seed"]);
|
|
925
|
+
if (!seed.ok) {
|
|
926
|
+
reject(seed.message);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const integrations = readIntegrations(body["integrations"]);
|
|
930
|
+
if (!integrations.ok) {
|
|
931
|
+
reject(integrations.message);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const jobMethod = readMethod(body["method"]);
|
|
935
|
+
if (!jobMethod.ok) {
|
|
936
|
+
reject(jobMethod.message);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const metadata = readMetadata(body["metadata"]);
|
|
940
|
+
if (!metadata.ok) {
|
|
941
|
+
reject(metadata.message);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const id = require_helpers.generateId("ftjob");
|
|
945
|
+
const job = {
|
|
946
|
+
id,
|
|
947
|
+
object: "fine_tuning.job",
|
|
948
|
+
model,
|
|
949
|
+
training_file: trainingFile,
|
|
950
|
+
validation_file: typeof validationFile === "string" ? validationFile : null,
|
|
951
|
+
status: "validating_files",
|
|
952
|
+
created_at: stamp(),
|
|
953
|
+
finished_at: null,
|
|
954
|
+
fine_tuned_model: null,
|
|
955
|
+
hyperparameters,
|
|
956
|
+
error: null,
|
|
957
|
+
organization_id: ORGANIZATION_ID,
|
|
958
|
+
result_files: [],
|
|
959
|
+
seed: seed.value ?? seedFor(id),
|
|
960
|
+
trained_tokens: null
|
|
961
|
+
};
|
|
962
|
+
if (integrations.value !== null) job.integrations = integrations.value;
|
|
963
|
+
if (jobMethod.value !== null) job.method = jobMethod.value;
|
|
964
|
+
if (metadata.value !== null) job.metadata = metadata.value;
|
|
965
|
+
if (suffix.value !== null) suffixes.set(job.id, suffix.value);
|
|
966
|
+
jobs.set(job.id, job);
|
|
967
|
+
polls.set(job.id, 0);
|
|
968
|
+
appendEvent(job, "Job created, validating training file");
|
|
969
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 200, journaledBody);
|
|
970
|
+
writeJson(res, 200, job, setCorsHeaders);
|
|
971
|
+
}
|
|
972
|
+
async function handleFineTuningList(req, res, journal, defaults, setCorsHeaders) {
|
|
973
|
+
const path = req.url ?? "/v1/fine_tuning/jobs";
|
|
974
|
+
const method = req.method ?? "GET";
|
|
975
|
+
if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
|
|
976
|
+
writePage(res, journal, req, method, path, [...jobs.values()].reverse(), setCorsHeaders);
|
|
977
|
+
}
|
|
978
|
+
async function handleFineTuningRetrieve(req, res, id, journal, defaults, setCorsHeaders) {
|
|
979
|
+
const path = req.url ?? `/v1/fine_tuning/jobs/${id}`;
|
|
980
|
+
const method = req.method ?? "GET";
|
|
981
|
+
if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
|
|
982
|
+
const job = jobs.get(id);
|
|
983
|
+
if (!job) {
|
|
984
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
|
|
985
|
+
writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
|
|
989
|
+
writeJson(res, 200, advance(job), setCorsHeaders);
|
|
990
|
+
}
|
|
991
|
+
async function handleFineTuningCancel(req, res, id, journal, defaults, setCorsHeaders) {
|
|
992
|
+
const path = req.url ?? `/v1/fine_tuning/jobs/${id}/cancel`;
|
|
993
|
+
const method = req.method ?? "POST";
|
|
994
|
+
if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
|
|
995
|
+
const job = jobs.get(id);
|
|
996
|
+
if (!job) {
|
|
997
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
|
|
998
|
+
writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (isTerminal(job.status)) {
|
|
1002
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 400);
|
|
1003
|
+
writeJson(res, 400, invalid(`Job ${id} is already terminal (${job.status})`), setCorsHeaders);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
job.status = "cancelled";
|
|
1007
|
+
job.finished_at = appendEvent(job, "Job cancelled by user");
|
|
1008
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
|
|
1009
|
+
writeJson(res, 200, job, setCorsHeaders);
|
|
1010
|
+
}
|
|
1011
|
+
async function handleFineTuningEvents(req, res, id, journal, defaults, setCorsHeaders) {
|
|
1012
|
+
const path = req.url ?? `/v1/fine_tuning/jobs/${id}/events`;
|
|
1013
|
+
const method = req.method ?? "GET";
|
|
1014
|
+
if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
|
|
1015
|
+
const job = jobs.get(id);
|
|
1016
|
+
if (!job) {
|
|
1017
|
+
journalFt(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
|
|
1018
|
+
writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
writePage(res, journal, req, method, path, eventsFor(job).reverse(), setCorsHeaders);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
//#endregion
|
|
1025
|
+
exports.clearFineTuningStore = clearFineTuningStore;
|
|
1026
|
+
exports.handleFineTuningCancel = handleFineTuningCancel;
|
|
1027
|
+
exports.handleFineTuningCreate = handleFineTuningCreate;
|
|
1028
|
+
exports.handleFineTuningEvents = handleFineTuningEvents;
|
|
1029
|
+
exports.handleFineTuningList = handleFineTuningList;
|
|
1030
|
+
exports.handleFineTuningRetrieve = handleFineTuningRetrieve;
|
|
1031
|
+
exports.paginate = paginate;
|
|
1032
|
+
exports.readMetadata = readMetadata;
|
|
1033
|
+
//# sourceMappingURL=fine-tuning.cjs.map
|