@vellumai/assistant 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +71 -8
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
export interface LlmContextNormalizationInput {
|
|
2
|
+
requestPayload: unknown;
|
|
3
|
+
responsePayload: unknown;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LlmContextSummary {
|
|
8
|
+
provider: "openai" | "anthropic" | "gemini";
|
|
9
|
+
model?: string;
|
|
10
|
+
inputTokens?: number;
|
|
11
|
+
outputTokens?: number;
|
|
12
|
+
cacheCreationInputTokens?: number;
|
|
13
|
+
cacheReadInputTokens?: number;
|
|
14
|
+
stopReason?: string;
|
|
15
|
+
requestMessageCount?: number;
|
|
16
|
+
requestToolCount?: number;
|
|
17
|
+
responseMessageCount?: number;
|
|
18
|
+
responseToolCallCount?: number;
|
|
19
|
+
responsePreview?: string;
|
|
20
|
+
toolCallNames?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LlmContextSection {
|
|
24
|
+
kind:
|
|
25
|
+
| "system"
|
|
26
|
+
| "message"
|
|
27
|
+
| "reasoning"
|
|
28
|
+
| "settings"
|
|
29
|
+
| "tool_definitions"
|
|
30
|
+
| "tool_use"
|
|
31
|
+
| "tool_result"
|
|
32
|
+
| "function_call"
|
|
33
|
+
| "function_response";
|
|
34
|
+
label: string;
|
|
35
|
+
role?: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
toolName?: string;
|
|
38
|
+
data?: unknown;
|
|
39
|
+
language?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LlmContextNormalizationResult {
|
|
43
|
+
summary?: LlmContextSummary;
|
|
44
|
+
requestSections?: LlmContextSection[];
|
|
45
|
+
responseSections?: LlmContextSection[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface NormalizedPayloadCandidate {
|
|
49
|
+
provider: LlmContextSummary["provider"];
|
|
50
|
+
summary: LlmContextSummary;
|
|
51
|
+
requestSections?: LlmContextSection[];
|
|
52
|
+
responseSections?: LlmContextSection[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeLlmContextPayloads(
|
|
56
|
+
input: LlmContextNormalizationInput,
|
|
57
|
+
): LlmContextNormalizationResult {
|
|
58
|
+
const requestCandidates = [
|
|
59
|
+
normalizeOpenAiRequestPayload(input.requestPayload),
|
|
60
|
+
normalizeAnthropicRequestPayload(input.requestPayload),
|
|
61
|
+
normalizeGeminiRequestPayload(input.requestPayload),
|
|
62
|
+
].filter((candidate): candidate is NormalizedPayloadCandidate =>
|
|
63
|
+
Boolean(candidate),
|
|
64
|
+
);
|
|
65
|
+
const responseCandidates = [
|
|
66
|
+
normalizeOpenAiResponsePayload(input.responsePayload),
|
|
67
|
+
normalizeAnthropicResponsePayload(input.responsePayload),
|
|
68
|
+
normalizeGeminiResponsePayload(input.responsePayload),
|
|
69
|
+
].filter((candidate): candidate is NormalizedPayloadCandidate =>
|
|
70
|
+
Boolean(candidate),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (requestCandidates.length > 1 || responseCandidates.length > 1) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const requestCandidate = requestCandidates[0];
|
|
78
|
+
const responseCandidate = responseCandidates[0];
|
|
79
|
+
|
|
80
|
+
if (requestCandidate && responseCandidate) {
|
|
81
|
+
if (requestCandidate.provider !== responseCandidate.provider) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return mergeNormalizedCandidates(requestCandidate, responseCandidate);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (requestCandidate) {
|
|
89
|
+
return requestCandidate as LlmContextNormalizationResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (responseCandidate) {
|
|
93
|
+
const requestCandidate = normalizeCompatibleRequestPayload(
|
|
94
|
+
input.requestPayload,
|
|
95
|
+
responseCandidate.provider,
|
|
96
|
+
);
|
|
97
|
+
if (
|
|
98
|
+
requestCandidate &&
|
|
99
|
+
requestCandidate.provider !== responseCandidate.provider
|
|
100
|
+
) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return mergeNormalizedCandidates(requestCandidate, responseCandidate);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeOpenAiRequestPayload(
|
|
111
|
+
requestPayload: unknown,
|
|
112
|
+
allowPlainText = false,
|
|
113
|
+
): NormalizedPayloadCandidate | null {
|
|
114
|
+
const request = asRecord(requestPayload);
|
|
115
|
+
if (!request) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const messages = asRecordArray(request.messages);
|
|
120
|
+
if (!messages) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requestToolNames = extractOpenAiRequestToolNames(request.tools);
|
|
125
|
+
const hasOpenAiSignal =
|
|
126
|
+
requestToolNames.length > 0 ||
|
|
127
|
+
asString(request.tool_choice) !== undefined ||
|
|
128
|
+
(request.parallel_tool_calls !== undefined &&
|
|
129
|
+
typeof request.parallel_tool_calls === "boolean") ||
|
|
130
|
+
messages.some((message) => Boolean(asRecordArray(message.tool_calls)));
|
|
131
|
+
if (!allowPlainText && !hasOpenAiSignal) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const requestSections: LlmContextSection[] = [];
|
|
136
|
+
for (const [index, message] of messages.entries()) {
|
|
137
|
+
const role = asString(message.role) ?? "unknown";
|
|
138
|
+
const messageText = extractOpenAiContentText(message.content);
|
|
139
|
+
if (messageText !== undefined) {
|
|
140
|
+
requestSections.push({
|
|
141
|
+
kind:
|
|
142
|
+
role === "system"
|
|
143
|
+
? "system"
|
|
144
|
+
: role === "tool"
|
|
145
|
+
? "tool_result"
|
|
146
|
+
: "message",
|
|
147
|
+
label: buildMessageLabel(role, index + 1),
|
|
148
|
+
role,
|
|
149
|
+
text: messageText,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const toolCallSection of openAiToolCallSections(
|
|
154
|
+
message.tool_calls,
|
|
155
|
+
"Request tool call",
|
|
156
|
+
)) {
|
|
157
|
+
requestSections.push(toolCallSection);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (requestToolNames.length > 0) {
|
|
162
|
+
requestSections.push({
|
|
163
|
+
kind: "tool_definitions",
|
|
164
|
+
label: "Available tools",
|
|
165
|
+
data: {
|
|
166
|
+
tools: asRecordArray(request.tools) ?? request.tools,
|
|
167
|
+
},
|
|
168
|
+
language: "json",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const requestSettings = omitRecordKeys(request, ["messages", "tools"]);
|
|
173
|
+
if (hasMeaningfulRequestSettings(requestSettings)) {
|
|
174
|
+
requestSections.push(
|
|
175
|
+
structuredJsonSection("settings", "Request settings", requestSettings),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
provider: "openai",
|
|
181
|
+
summary: {
|
|
182
|
+
provider: "openai",
|
|
183
|
+
model: asString(request.model),
|
|
184
|
+
inputTokens: undefined,
|
|
185
|
+
outputTokens: undefined,
|
|
186
|
+
cacheCreationInputTokens: undefined,
|
|
187
|
+
cacheReadInputTokens: undefined,
|
|
188
|
+
stopReason: undefined,
|
|
189
|
+
requestMessageCount: messages.length,
|
|
190
|
+
requestToolCount: requestToolNames.length,
|
|
191
|
+
responseMessageCount: undefined,
|
|
192
|
+
responseToolCallCount: undefined,
|
|
193
|
+
responsePreview: undefined,
|
|
194
|
+
toolCallNames: undefined,
|
|
195
|
+
},
|
|
196
|
+
requestSections: requestSections.length > 0 ? requestSections : undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeOpenAiResponsePayload(
|
|
201
|
+
responsePayload: unknown,
|
|
202
|
+
): NormalizedPayloadCandidate | null {
|
|
203
|
+
const response = asRecord(responsePayload);
|
|
204
|
+
if (!response) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const choices = asRecordArray(response.choices);
|
|
209
|
+
if (!choices) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const firstChoice = choices[0];
|
|
214
|
+
const responseMessage = asRecord(firstChoice?.message);
|
|
215
|
+
const responseText = extractOpenAiContentText(responseMessage?.content);
|
|
216
|
+
const responseSections: LlmContextSection[] = [];
|
|
217
|
+
if (responseText !== undefined) {
|
|
218
|
+
responseSections.push({
|
|
219
|
+
kind: "message",
|
|
220
|
+
label: "Assistant response",
|
|
221
|
+
role: asString(responseMessage?.role) ?? "assistant",
|
|
222
|
+
text: responseText,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const responseToolSections = openAiToolCallSections(
|
|
226
|
+
responseMessage?.tool_calls,
|
|
227
|
+
"Response tool call",
|
|
228
|
+
);
|
|
229
|
+
responseSections.push(...responseToolSections);
|
|
230
|
+
|
|
231
|
+
const usage = asRecord(response.usage);
|
|
232
|
+
const toolCallNames = responseToolSections
|
|
233
|
+
.map((section) => section.toolName)
|
|
234
|
+
.filter((name): name is string => typeof name === "string");
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
provider: "openai",
|
|
238
|
+
summary: {
|
|
239
|
+
provider: "openai",
|
|
240
|
+
model: asString(response.model),
|
|
241
|
+
inputTokens: asNumber(usage?.prompt_tokens),
|
|
242
|
+
outputTokens: asNumber(usage?.completion_tokens),
|
|
243
|
+
cacheCreationInputTokens: undefined,
|
|
244
|
+
cacheReadInputTokens: undefined,
|
|
245
|
+
stopReason: asString(firstChoice?.finish_reason),
|
|
246
|
+
requestMessageCount: undefined,
|
|
247
|
+
requestToolCount: undefined,
|
|
248
|
+
responseMessageCount:
|
|
249
|
+
responseText !== undefined || responseToolSections.length > 0
|
|
250
|
+
? 1
|
|
251
|
+
: undefined,
|
|
252
|
+
responseToolCallCount:
|
|
253
|
+
responseToolSections.length > 0
|
|
254
|
+
? responseToolSections.length
|
|
255
|
+
: undefined,
|
|
256
|
+
responsePreview: responseText ? truncateText(responseText) : undefined,
|
|
257
|
+
toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined,
|
|
258
|
+
},
|
|
259
|
+
responseSections:
|
|
260
|
+
responseSections.length > 0 ? responseSections : undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeAnthropicRequestPayload(
|
|
265
|
+
requestPayload: unknown,
|
|
266
|
+
allowPlainText = false,
|
|
267
|
+
): NormalizedPayloadCandidate | null {
|
|
268
|
+
const request = asRecord(requestPayload);
|
|
269
|
+
if (!request) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const messages = asRecordArray(request.messages);
|
|
274
|
+
if (!messages) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const requestToolNames = extractAnthropicToolNames(request.tools);
|
|
279
|
+
const hasAnthropicContentSignal = messages.some((message) =>
|
|
280
|
+
(asRecordArray(message.content) ?? []).some((block) => {
|
|
281
|
+
const type = asString(block.type);
|
|
282
|
+
return (
|
|
283
|
+
type === "document" ||
|
|
284
|
+
type === "tool_use" ||
|
|
285
|
+
type === "server_tool_use" ||
|
|
286
|
+
type === "tool_result" ||
|
|
287
|
+
type === "web_search_tool_result" ||
|
|
288
|
+
type === "thinking" ||
|
|
289
|
+
type === "redacted_thinking"
|
|
290
|
+
);
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
const hasAnthropicSignal =
|
|
294
|
+
request.system !== undefined ||
|
|
295
|
+
requestToolNames.length > 0 ||
|
|
296
|
+
isAnthropicToolChoice(request.tool_choice) ||
|
|
297
|
+
hasAnthropicContentSignal;
|
|
298
|
+
if (!allowPlainText && !hasAnthropicSignal) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const requestSections: LlmContextSection[] = [];
|
|
303
|
+
const systemSections = anthropicSystemSections(request.system);
|
|
304
|
+
requestSections.push(...systemSections);
|
|
305
|
+
|
|
306
|
+
for (const [index, message] of messages.entries()) {
|
|
307
|
+
requestSections.push(
|
|
308
|
+
...anthropicMessageSections(
|
|
309
|
+
message,
|
|
310
|
+
buildMessageLabel(asString(message.role) ?? "unknown", index + 1),
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (requestToolNames.length > 0) {
|
|
316
|
+
requestSections.push({
|
|
317
|
+
kind: "tool_definitions",
|
|
318
|
+
label: "Available tools",
|
|
319
|
+
data: {
|
|
320
|
+
tools: asRecordArray(request.tools) ?? request.tools,
|
|
321
|
+
},
|
|
322
|
+
language: "json",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const requestSettings = omitRecordKeys(request, [
|
|
327
|
+
"system",
|
|
328
|
+
"messages",
|
|
329
|
+
"tools",
|
|
330
|
+
]);
|
|
331
|
+
if (hasMeaningfulRequestSettings(requestSettings)) {
|
|
332
|
+
requestSections.push(
|
|
333
|
+
structuredJsonSection("settings", "Request settings", requestSettings),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
provider: "anthropic",
|
|
339
|
+
summary: {
|
|
340
|
+
provider: "anthropic",
|
|
341
|
+
model: asString(request.model),
|
|
342
|
+
inputTokens: undefined,
|
|
343
|
+
outputTokens: undefined,
|
|
344
|
+
cacheCreationInputTokens: undefined,
|
|
345
|
+
cacheReadInputTokens: undefined,
|
|
346
|
+
stopReason: undefined,
|
|
347
|
+
requestMessageCount: messages.length,
|
|
348
|
+
requestToolCount: requestToolNames.length,
|
|
349
|
+
responseMessageCount: undefined,
|
|
350
|
+
responseToolCallCount: undefined,
|
|
351
|
+
responsePreview: undefined,
|
|
352
|
+
toolCallNames: undefined,
|
|
353
|
+
},
|
|
354
|
+
requestSections: requestSections.length > 0 ? requestSections : undefined,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function normalizeAnthropicResponsePayload(
|
|
359
|
+
responsePayload: unknown,
|
|
360
|
+
): NormalizedPayloadCandidate | null {
|
|
361
|
+
const response = asRecord(responsePayload);
|
|
362
|
+
if (!response) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const content = asRecordArray(response.content);
|
|
367
|
+
if (!content) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const responseSections = anthropicContentSections(
|
|
372
|
+
content,
|
|
373
|
+
"Assistant response",
|
|
374
|
+
);
|
|
375
|
+
const responseText = collectAnthropicPreviewText(content);
|
|
376
|
+
const responseToolNames = content
|
|
377
|
+
.map((block) =>
|
|
378
|
+
isAnthropicToolUseType(asString(block.type))
|
|
379
|
+
? asString(block.name)
|
|
380
|
+
: undefined,
|
|
381
|
+
)
|
|
382
|
+
.filter((name): name is string => typeof name === "string");
|
|
383
|
+
const hasAnthropicResponseMessage = responseSections.some(
|
|
384
|
+
(section) =>
|
|
385
|
+
section.kind === "message" ||
|
|
386
|
+
section.kind === "tool_use" ||
|
|
387
|
+
section.kind === "reasoning",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const usage = asRecord(response.usage);
|
|
391
|
+
return {
|
|
392
|
+
provider: "anthropic",
|
|
393
|
+
summary: {
|
|
394
|
+
provider: "anthropic",
|
|
395
|
+
model: asString(response.model),
|
|
396
|
+
inputTokens: asNumber(usage?.input_tokens),
|
|
397
|
+
outputTokens: asNumber(usage?.output_tokens),
|
|
398
|
+
cacheCreationInputTokens: asNumber(usage?.cache_creation_input_tokens),
|
|
399
|
+
cacheReadInputTokens: asNumber(usage?.cache_read_input_tokens),
|
|
400
|
+
stopReason: asString(response.stop_reason),
|
|
401
|
+
requestMessageCount: undefined,
|
|
402
|
+
requestToolCount: undefined,
|
|
403
|
+
responseMessageCount: hasAnthropicResponseMessage ? 1 : undefined,
|
|
404
|
+
responseToolCallCount:
|
|
405
|
+
responseToolNames.length > 0 ? responseToolNames.length : undefined,
|
|
406
|
+
responsePreview: responseText ? truncateText(responseText) : undefined,
|
|
407
|
+
toolCallNames:
|
|
408
|
+
responseToolNames.length > 0 ? responseToolNames : undefined,
|
|
409
|
+
},
|
|
410
|
+
responseSections:
|
|
411
|
+
responseSections.length > 0 ? responseSections : undefined,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function normalizeGeminiRequestPayload(
|
|
416
|
+
requestPayload: unknown,
|
|
417
|
+
): NormalizedPayloadCandidate | null {
|
|
418
|
+
const request = asRecord(requestPayload);
|
|
419
|
+
if (!request) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const contents = asRecordArray(request.contents);
|
|
424
|
+
if (!contents) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const requestSections: LlmContextSection[] = [];
|
|
429
|
+
const config = asRecord(request.config);
|
|
430
|
+
const systemText = extractGeminiSystemInstructionText(
|
|
431
|
+
config?.systemInstruction,
|
|
432
|
+
);
|
|
433
|
+
if (systemText !== undefined) {
|
|
434
|
+
requestSections.push({
|
|
435
|
+
kind: "system",
|
|
436
|
+
label: "System instruction",
|
|
437
|
+
role: "system",
|
|
438
|
+
text: systemText,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const [index, content] of contents.entries()) {
|
|
443
|
+
requestSections.push(...geminiContentSections(content, index + 1));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const requestToolNames = extractGeminiToolNames(config?.tools);
|
|
447
|
+
if (requestToolNames.length > 0) {
|
|
448
|
+
requestSections.push({
|
|
449
|
+
kind: "tool_definitions",
|
|
450
|
+
label: "Available tools",
|
|
451
|
+
data: {
|
|
452
|
+
tools: asRecordArray(config?.tools) ?? config?.tools,
|
|
453
|
+
},
|
|
454
|
+
language: "json",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const requestSettings = buildGeminiRequestSettings(request, config);
|
|
459
|
+
if (hasMeaningfulRequestSettings(requestSettings)) {
|
|
460
|
+
requestSections.push(
|
|
461
|
+
structuredJsonSection("settings", "Generation config", requestSettings),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
provider: "gemini",
|
|
467
|
+
summary: {
|
|
468
|
+
provider: "gemini",
|
|
469
|
+
model: asString(request.model),
|
|
470
|
+
inputTokens: undefined,
|
|
471
|
+
outputTokens: undefined,
|
|
472
|
+
cacheCreationInputTokens: undefined,
|
|
473
|
+
cacheReadInputTokens: undefined,
|
|
474
|
+
stopReason: undefined,
|
|
475
|
+
requestMessageCount: contents.length,
|
|
476
|
+
requestToolCount: requestToolNames.length,
|
|
477
|
+
responseMessageCount: undefined,
|
|
478
|
+
responseToolCallCount: undefined,
|
|
479
|
+
responsePreview: undefined,
|
|
480
|
+
toolCallNames: undefined,
|
|
481
|
+
},
|
|
482
|
+
requestSections: requestSections.length > 0 ? requestSections : undefined,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normalizeGeminiResponsePayload(
|
|
487
|
+
responsePayload: unknown,
|
|
488
|
+
): NormalizedPayloadCandidate | null {
|
|
489
|
+
const response = asRecord(responsePayload);
|
|
490
|
+
if (!response) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const responseText = asString(response.text);
|
|
495
|
+
const responseFunctionSections = geminiFunctionCallSections(
|
|
496
|
+
response.functionCalls,
|
|
497
|
+
"Response function call",
|
|
498
|
+
);
|
|
499
|
+
const usage = asRecord(response.usageMetadata);
|
|
500
|
+
if (responseText === undefined && responseFunctionSections.length === 0) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const responseSections: LlmContextSection[] = [];
|
|
505
|
+
if (responseText !== undefined) {
|
|
506
|
+
responseSections.push({
|
|
507
|
+
kind: "message",
|
|
508
|
+
label: "Assistant response",
|
|
509
|
+
role: "model",
|
|
510
|
+
text: responseText,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
responseSections.push(...responseFunctionSections);
|
|
514
|
+
|
|
515
|
+
const toolCallNames = responseFunctionSections
|
|
516
|
+
.map((section) => section.toolName)
|
|
517
|
+
.filter((name): name is string => typeof name === "string");
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
provider: "gemini",
|
|
521
|
+
summary: {
|
|
522
|
+
provider: "gemini",
|
|
523
|
+
model: asString(response.model),
|
|
524
|
+
inputTokens: asNumber(usage?.promptTokenCount),
|
|
525
|
+
outputTokens: asNumber(usage?.candidatesTokenCount),
|
|
526
|
+
cacheCreationInputTokens: undefined,
|
|
527
|
+
cacheReadInputTokens: undefined,
|
|
528
|
+
stopReason: asString(response.finishReason),
|
|
529
|
+
requestMessageCount: undefined,
|
|
530
|
+
requestToolCount: undefined,
|
|
531
|
+
responseMessageCount:
|
|
532
|
+
responseText !== undefined || responseFunctionSections.length > 0
|
|
533
|
+
? 1
|
|
534
|
+
: undefined,
|
|
535
|
+
responseToolCallCount:
|
|
536
|
+
responseFunctionSections.length > 0
|
|
537
|
+
? responseFunctionSections.length
|
|
538
|
+
: undefined,
|
|
539
|
+
responsePreview: responseText ? truncateText(responseText) : undefined,
|
|
540
|
+
toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined,
|
|
541
|
+
},
|
|
542
|
+
responseSections:
|
|
543
|
+
responseSections.length > 0 ? responseSections : undefined,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function anthropicSystemSections(system: unknown): LlmContextSection[] {
|
|
548
|
+
const text = extractAnthropicSystemText(system);
|
|
549
|
+
if (!text) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
return [
|
|
553
|
+
{
|
|
554
|
+
kind: "system",
|
|
555
|
+
label: "System prompt",
|
|
556
|
+
role: "system",
|
|
557
|
+
text,
|
|
558
|
+
},
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function anthropicMessageSections(
|
|
563
|
+
message: Record<string, unknown>,
|
|
564
|
+
label: string,
|
|
565
|
+
): LlmContextSection[] {
|
|
566
|
+
const role = asString(message.role) ?? "unknown";
|
|
567
|
+
const content = message.content;
|
|
568
|
+
const sections: LlmContextSection[] = [];
|
|
569
|
+
const text = collectAnthropicMessageText(content);
|
|
570
|
+
if (text) {
|
|
571
|
+
sections.push({
|
|
572
|
+
kind: "message",
|
|
573
|
+
label,
|
|
574
|
+
role,
|
|
575
|
+
text,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
for (const block of asRecordArray(content) ?? []) {
|
|
580
|
+
const type = asString(block.type);
|
|
581
|
+
if (type === "thinking" || type === "redacted_thinking") {
|
|
582
|
+
sections.push({
|
|
583
|
+
kind: "reasoning",
|
|
584
|
+
label: `${label} reasoning`,
|
|
585
|
+
role,
|
|
586
|
+
text: collectAnthropicReasoningText(block),
|
|
587
|
+
});
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (isAnthropicToolUseType(type)) {
|
|
592
|
+
sections.push({
|
|
593
|
+
kind: "tool_use",
|
|
594
|
+
label: `${label} tool use`,
|
|
595
|
+
role,
|
|
596
|
+
toolName: asString(block.name),
|
|
597
|
+
data: asRecord(block.input) ?? block.input,
|
|
598
|
+
text: previewStructuredValue(block.input),
|
|
599
|
+
});
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (isAnthropicToolResultType(type)) {
|
|
604
|
+
sections.push({
|
|
605
|
+
kind: "tool_result",
|
|
606
|
+
label: `${label} tool result`,
|
|
607
|
+
role,
|
|
608
|
+
toolName: asString(block.name) ?? asString(block.tool_use_id),
|
|
609
|
+
data:
|
|
610
|
+
type === "web_search_tool_result"
|
|
611
|
+
? sanitizeAnthropicWebSearchToolResultData(block)
|
|
612
|
+
: undefined,
|
|
613
|
+
text: collectAnthropicToolResultText(block),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return sections;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function anthropicContentSections(
|
|
622
|
+
content: Record<string, unknown>[],
|
|
623
|
+
label: string,
|
|
624
|
+
): LlmContextSection[] {
|
|
625
|
+
return anthropicMessageSections(
|
|
626
|
+
{
|
|
627
|
+
role: "assistant",
|
|
628
|
+
content,
|
|
629
|
+
},
|
|
630
|
+
label,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function geminiContentSections(
|
|
635
|
+
content: Record<string, unknown>,
|
|
636
|
+
index: number,
|
|
637
|
+
): LlmContextSection[] {
|
|
638
|
+
const role = asString(content.role) ?? "unknown";
|
|
639
|
+
const parts = asRecordArray(content.parts) ?? [];
|
|
640
|
+
const sections: LlmContextSection[] = [];
|
|
641
|
+
const textParts: string[] = [];
|
|
642
|
+
|
|
643
|
+
for (const part of parts) {
|
|
644
|
+
const text = asString(part.text);
|
|
645
|
+
if (text) {
|
|
646
|
+
textParts.push(text);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const inlineData = asRecord(part.inlineData);
|
|
651
|
+
if (inlineData) {
|
|
652
|
+
const mimeType =
|
|
653
|
+
asString(inlineData.mimeType) ?? "application/octet-stream";
|
|
654
|
+
textParts.push(`[inline data: ${mimeType}]`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const functionCall = asRecord(part.functionCall);
|
|
659
|
+
if (functionCall) {
|
|
660
|
+
sections.push({
|
|
661
|
+
kind: "function_call",
|
|
662
|
+
label: `${buildMessageLabel(role, index)} function call`,
|
|
663
|
+
role,
|
|
664
|
+
toolName: asString(functionCall.name),
|
|
665
|
+
data: asRecord(functionCall.args) ?? functionCall.args,
|
|
666
|
+
text: previewStructuredValue(functionCall.args),
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const functionResponse = asRecord(part.functionResponse);
|
|
672
|
+
if (functionResponse) {
|
|
673
|
+
sections.push({
|
|
674
|
+
kind: "function_response",
|
|
675
|
+
label: `${buildMessageLabel(role, index)} function response`,
|
|
676
|
+
role,
|
|
677
|
+
toolName: asString(functionResponse.name),
|
|
678
|
+
data: asRecord(functionResponse.response) ?? functionResponse.response,
|
|
679
|
+
text: previewStructuredValue(functionResponse.response),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const text = joinTextParts(textParts);
|
|
685
|
+
if (text) {
|
|
686
|
+
sections.unshift({
|
|
687
|
+
kind: "message",
|
|
688
|
+
label: buildMessageLabel(role, index),
|
|
689
|
+
role,
|
|
690
|
+
text,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return sections;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function openAiToolCallSections(
|
|
698
|
+
toolCalls: unknown,
|
|
699
|
+
labelPrefix: string,
|
|
700
|
+
): LlmContextSection[] {
|
|
701
|
+
return (asRecordArray(toolCalls) ?? []).map((toolCall, index) => {
|
|
702
|
+
const fn = asRecord(toolCall.function);
|
|
703
|
+
return {
|
|
704
|
+
kind: "function_call",
|
|
705
|
+
label: `${labelPrefix} ${index + 1}`,
|
|
706
|
+
role: "assistant",
|
|
707
|
+
toolName: asString(fn?.name),
|
|
708
|
+
data: parseJsonValue(asString(fn?.arguments)),
|
|
709
|
+
text: previewStructuredValue(parseJsonValue(asString(fn?.arguments))),
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function geminiFunctionCallSections(
|
|
715
|
+
functionCalls: unknown,
|
|
716
|
+
labelPrefix: string,
|
|
717
|
+
): LlmContextSection[] {
|
|
718
|
+
return (asRecordArray(functionCalls) ?? []).map((call, index) => ({
|
|
719
|
+
kind: "function_call",
|
|
720
|
+
label: `${labelPrefix} ${index + 1}`,
|
|
721
|
+
role: "model",
|
|
722
|
+
toolName: asString(call.name),
|
|
723
|
+
data: asRecord(call.args) ?? call.args,
|
|
724
|
+
text: previewStructuredValue(call.args),
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function extractOpenAiRequestToolNames(tools: unknown): string[] {
|
|
729
|
+
return (asRecordArray(tools) ?? [])
|
|
730
|
+
.map((tool) => asString(asRecord(tool.function)?.name))
|
|
731
|
+
.filter((name): name is string => typeof name === "string");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function extractAnthropicToolNames(tools: unknown): string[] {
|
|
735
|
+
return (asRecordArray(tools) ?? [])
|
|
736
|
+
.map((tool) => asString(tool.name))
|
|
737
|
+
.filter((name): name is string => typeof name === "string");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function isAnthropicToolChoice(toolChoice: unknown): boolean {
|
|
741
|
+
const record = asRecord(toolChoice);
|
|
742
|
+
if (!record) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const type = asString(record.type);
|
|
747
|
+
return (
|
|
748
|
+
type === "auto" || type === "any" || type === "tool" || type === "none"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function extractGeminiToolNames(tools: unknown): string[] {
|
|
753
|
+
const toolGroups = asRecordArray(tools) ?? [];
|
|
754
|
+
const names: string[] = [];
|
|
755
|
+
for (const toolGroup of toolGroups) {
|
|
756
|
+
for (const declaration of asRecordArray(toolGroup.functionDeclarations) ??
|
|
757
|
+
[]) {
|
|
758
|
+
const name = asString(declaration.name);
|
|
759
|
+
if (name) {
|
|
760
|
+
names.push(name);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return names;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function extractOpenAiContentText(content: unknown): string | undefined {
|
|
768
|
+
if (typeof content === "string") {
|
|
769
|
+
return hasMeaningfulText(content) ? content : undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const parts = asRecordArray(content);
|
|
773
|
+
if (!parts) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const textParts: string[] = [];
|
|
778
|
+
for (const part of parts) {
|
|
779
|
+
const type = asString(part.type);
|
|
780
|
+
if (type === "text" || type === "input_text" || type === "output_text") {
|
|
781
|
+
const text = asString(part.text);
|
|
782
|
+
if (text) {
|
|
783
|
+
textParts.push(text);
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (type === "image_url" || type === "input_image") {
|
|
789
|
+
textParts.push("[image]");
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (type === "file") {
|
|
794
|
+
textParts.push("[file]");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return joinTextParts(textParts);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function extractAnthropicSystemText(system: unknown): string | undefined {
|
|
802
|
+
if (typeof system === "string") {
|
|
803
|
+
return hasMeaningfulText(system) ? system : undefined;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const parts = asRecordArray(system);
|
|
807
|
+
if (!parts) {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const textParts = parts
|
|
812
|
+
.map((part) => asString(part.text))
|
|
813
|
+
.filter((text): text is string => typeof text === "string");
|
|
814
|
+
return joinTextParts(textParts);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function extractGeminiSystemInstructionText(
|
|
818
|
+
systemInstruction: unknown,
|
|
819
|
+
): string | undefined {
|
|
820
|
+
if (typeof systemInstruction === "string") {
|
|
821
|
+
return hasMeaningfulText(systemInstruction) ? systemInstruction : undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const record = asRecord(systemInstruction);
|
|
825
|
+
if (!record) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const parts = asRecordArray(record.parts) ?? [];
|
|
830
|
+
const textParts = parts
|
|
831
|
+
.map((part) => asString(part.text))
|
|
832
|
+
.filter((text): text is string => typeof text === "string");
|
|
833
|
+
return joinTextParts(textParts);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function collectAnthropicText(content: unknown): string | undefined {
|
|
837
|
+
if (typeof content === "string") {
|
|
838
|
+
return hasMeaningfulText(content) ? content : undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const blocks = asRecordArray(content);
|
|
842
|
+
if (!blocks) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const textParts: string[] = [];
|
|
847
|
+
for (const block of blocks) {
|
|
848
|
+
const text = collectAnthropicBlockText(block);
|
|
849
|
+
if (text) {
|
|
850
|
+
textParts.push(text);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return joinTextParts(textParts);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function collectAnthropicMessageText(content: unknown): string | undefined {
|
|
858
|
+
if (typeof content === "string") {
|
|
859
|
+
return hasMeaningfulText(content) ? content : undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const blocks = asRecordArray(content);
|
|
863
|
+
if (!blocks) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const textParts: string[] = [];
|
|
868
|
+
for (const block of blocks) {
|
|
869
|
+
const text = collectAnthropicBlockText(block);
|
|
870
|
+
if (text) {
|
|
871
|
+
textParts.push(text);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return joinTextParts(textParts);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function collectAnthropicPreviewText(content: unknown): string | undefined {
|
|
879
|
+
if (typeof content === "string") {
|
|
880
|
+
return hasMeaningfulText(content) ? content : undefined;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const blocks = asRecordArray(content);
|
|
884
|
+
if (!blocks) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const textParts: string[] = [];
|
|
889
|
+
for (const block of blocks) {
|
|
890
|
+
if (asString(block.type) !== "text") {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const text = asString(block.text);
|
|
895
|
+
if (text) {
|
|
896
|
+
textParts.push(text);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return joinTextParts(textParts);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function collectAnthropicBlockText(
|
|
904
|
+
block: Record<string, unknown>,
|
|
905
|
+
): string | undefined {
|
|
906
|
+
const type = asString(block.type);
|
|
907
|
+
if (type === "text") {
|
|
908
|
+
const text = asString(block.text);
|
|
909
|
+
return text ? text : undefined;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (type === "image") {
|
|
913
|
+
return "[image]";
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (type === "document") {
|
|
917
|
+
const title = asString(block.title);
|
|
918
|
+
return title ? `[document: ${title}]` : "[document]";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function collectAnthropicReasoningText(
|
|
925
|
+
block: Record<string, unknown>,
|
|
926
|
+
): string | undefined {
|
|
927
|
+
const type = asString(block.type);
|
|
928
|
+
if (type === "thinking") {
|
|
929
|
+
const thinking = asString(block.thinking);
|
|
930
|
+
return thinking ? thinking : undefined;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (type === "redacted_thinking") {
|
|
934
|
+
return "[redacted thinking]";
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function isAnthropicToolUseType(type: string | undefined): boolean {
|
|
941
|
+
return type === "tool_use" || type === "server_tool_use";
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function isAnthropicToolResultType(type: string | undefined): boolean {
|
|
945
|
+
return type === "tool_result" || type === "web_search_tool_result";
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function collectAnthropicToolResultText(
|
|
949
|
+
block: Record<string, unknown>,
|
|
950
|
+
): string | undefined {
|
|
951
|
+
if (asString(block.type) === "web_search_tool_result") {
|
|
952
|
+
return "[Web search results]";
|
|
953
|
+
}
|
|
954
|
+
return collectAnthropicText(block.content);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function sanitizeAnthropicWebSearchToolResultData(
|
|
958
|
+
block: Record<string, unknown>,
|
|
959
|
+
): unknown {
|
|
960
|
+
return sanitizeAnthropicStructuredValue(block);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function sanitizeAnthropicStructuredValue(value: unknown): unknown {
|
|
964
|
+
if (Array.isArray(value)) {
|
|
965
|
+
return value.map((entry) => sanitizeAnthropicStructuredValue(entry));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const record = asRecord(value);
|
|
969
|
+
if (!record) {
|
|
970
|
+
return value;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return Object.fromEntries(
|
|
974
|
+
Object.entries(record)
|
|
975
|
+
.filter(
|
|
976
|
+
([key, entryValue]) =>
|
|
977
|
+
key !== "encrypted_content" && entryValue !== undefined,
|
|
978
|
+
)
|
|
979
|
+
.map(([key, entryValue]) => [
|
|
980
|
+
key,
|
|
981
|
+
sanitizeAnthropicStructuredValue(entryValue),
|
|
982
|
+
]),
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function buildMessageLabel(role: string, index: number): string {
|
|
987
|
+
const capitalizedRole =
|
|
988
|
+
role.length > 0 ? role[0]!.toUpperCase() + role.slice(1) : "Message";
|
|
989
|
+
if (role === "system") {
|
|
990
|
+
return "System prompt";
|
|
991
|
+
}
|
|
992
|
+
return `${capitalizedRole} message ${index}`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function buildGeminiRequestSettings(
|
|
996
|
+
request: Record<string, unknown>,
|
|
997
|
+
config: Record<string, unknown> | null,
|
|
998
|
+
): Record<string, unknown> | undefined {
|
|
999
|
+
const topLevelSettings = omitRecordKeys(request, ["contents", "config"]);
|
|
1000
|
+
const configSettings = omitRecordKeys(config, ["systemInstruction", "tools"]);
|
|
1001
|
+
|
|
1002
|
+
if (!topLevelSettings && !configSettings) {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
...(topLevelSettings ?? {}),
|
|
1008
|
+
...(configSettings ? { config: configSettings } : {}),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function structuredJsonSection(
|
|
1013
|
+
kind: LlmContextSection["kind"],
|
|
1014
|
+
label: string,
|
|
1015
|
+
data: unknown,
|
|
1016
|
+
): LlmContextSection {
|
|
1017
|
+
return {
|
|
1018
|
+
kind,
|
|
1019
|
+
label,
|
|
1020
|
+
data,
|
|
1021
|
+
language: "json",
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function hasMeaningfulRequestSettings(
|
|
1026
|
+
settings: Record<string, unknown> | undefined,
|
|
1027
|
+
): settings is Record<string, unknown> {
|
|
1028
|
+
if (!settings) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const keys = Object.keys(settings);
|
|
1033
|
+
return !(keys.length === 1 && keys[0] === "model");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function omitRecordKeys(
|
|
1037
|
+
record: Record<string, unknown> | null,
|
|
1038
|
+
omittedKeys: string[],
|
|
1039
|
+
): Record<string, unknown> | undefined {
|
|
1040
|
+
if (!record) {
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const filteredEntries = Object.entries(record).filter(
|
|
1045
|
+
([key, value]) => !omittedKeys.includes(key) && value !== undefined,
|
|
1046
|
+
);
|
|
1047
|
+
if (filteredEntries.length === 0) {
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return Object.fromEntries(filteredEntries);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function previewStructuredValue(value: unknown): string | undefined {
|
|
1055
|
+
if (value === undefined) {
|
|
1056
|
+
return undefined;
|
|
1057
|
+
}
|
|
1058
|
+
if (typeof value === "string") {
|
|
1059
|
+
return truncateText(value);
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
return truncateText(JSON.stringify(value));
|
|
1063
|
+
} catch {
|
|
1064
|
+
return undefined;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function parseJsonValue(value: string | undefined): unknown {
|
|
1069
|
+
if (!value) {
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
return JSON.parse(value);
|
|
1074
|
+
} catch {
|
|
1075
|
+
return value;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function joinTextParts(parts: string[]): string | undefined {
|
|
1080
|
+
if (parts.length === 0) {
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
const text = parts.join("\n\n");
|
|
1084
|
+
return hasMeaningfulText(text) ? text : undefined;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function truncateText(text: string, maxLength = 280): string {
|
|
1088
|
+
const normalized = normalizeText(text);
|
|
1089
|
+
if (normalized.length <= maxLength) {
|
|
1090
|
+
return normalized;
|
|
1091
|
+
}
|
|
1092
|
+
return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function normalizeText(text: string): string {
|
|
1096
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1097
|
+
return normalized.length > 0 ? normalized : "";
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function hasMeaningfulText(text: string): boolean {
|
|
1101
|
+
return text.trim().length > 0;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function mergeSummaryFragments(
|
|
1105
|
+
requestSummary: LlmContextSummary | undefined,
|
|
1106
|
+
responseSummary: LlmContextSummary | undefined,
|
|
1107
|
+
): LlmContextSummary | undefined {
|
|
1108
|
+
if (!requestSummary && !responseSummary) {
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const summary = {
|
|
1113
|
+
...(requestSummary ?? responseSummary)!,
|
|
1114
|
+
} as LlmContextSummary;
|
|
1115
|
+
|
|
1116
|
+
if (!requestSummary || !responseSummary) {
|
|
1117
|
+
return summary;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
for (const [key, value] of Object.entries(responseSummary) as [
|
|
1121
|
+
keyof LlmContextSummary,
|
|
1122
|
+
LlmContextSummary[keyof LlmContextSummary],
|
|
1123
|
+
][]) {
|
|
1124
|
+
if (value !== undefined) {
|
|
1125
|
+
summary[key] = value as never;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return summary;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function mergeNormalizedCandidates(
|
|
1133
|
+
requestCandidate: NormalizedPayloadCandidate | null | undefined,
|
|
1134
|
+
responseCandidate: NormalizedPayloadCandidate | null | undefined,
|
|
1135
|
+
): LlmContextNormalizationResult {
|
|
1136
|
+
if (!requestCandidate && !responseCandidate) {
|
|
1137
|
+
return {};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const requestSections = [
|
|
1141
|
+
...(requestCandidate?.requestSections ?? []),
|
|
1142
|
+
...(responseCandidate?.requestSections ?? []),
|
|
1143
|
+
];
|
|
1144
|
+
const responseSections = [
|
|
1145
|
+
...(requestCandidate?.responseSections ?? []),
|
|
1146
|
+
...(responseCandidate?.responseSections ?? []),
|
|
1147
|
+
];
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
summary: mergeSummaryFragments(
|
|
1151
|
+
requestCandidate?.summary,
|
|
1152
|
+
responseCandidate?.summary,
|
|
1153
|
+
),
|
|
1154
|
+
requestSections: requestSections.length > 0 ? requestSections : undefined,
|
|
1155
|
+
responseSections:
|
|
1156
|
+
responseSections.length > 0 ? responseSections : undefined,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function normalizeCompatibleRequestPayload(
|
|
1161
|
+
requestPayload: unknown,
|
|
1162
|
+
provider: LlmContextSummary["provider"],
|
|
1163
|
+
): NormalizedPayloadCandidate | null {
|
|
1164
|
+
switch (provider) {
|
|
1165
|
+
case "openai":
|
|
1166
|
+
return normalizeOpenAiRequestPayload(requestPayload, true);
|
|
1167
|
+
case "anthropic":
|
|
1168
|
+
return normalizeAnthropicRequestPayload(requestPayload, true);
|
|
1169
|
+
case "gemini":
|
|
1170
|
+
return normalizeGeminiRequestPayload(requestPayload);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
1175
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
return value as Record<string, unknown>;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function asRecordArray(value: unknown): Record<string, unknown>[] | null {
|
|
1182
|
+
if (!Array.isArray(value)) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
return value.filter(
|
|
1186
|
+
(entry): entry is Record<string, unknown> =>
|
|
1187
|
+
typeof entry === "object" && entry != null && !Array.isArray(entry),
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function asString(value: unknown): string | undefined {
|
|
1192
|
+
return typeof value === "string" ? value : undefined;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function asNumber(value: unknown): number | undefined {
|
|
1196
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
1197
|
+
? value
|
|
1198
|
+
: undefined;
|
|
1199
|
+
}
|