@elevasis/core 0.7.1 → 0.8.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/dist/test-utils/index.d.ts +3122 -0
- package/dist/test-utils/index.js +386 -0
- package/package.json +6 -1
- package/src/README.md +39 -36
- package/src/__tests__/publish.test.ts +18 -13
- package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
- package/src/_gen/__tests__/scaffold-contracts.test.ts +47 -36
- package/src/_gen/scaffold-contracts.ts +45 -45
- package/src/auth/multi-tenancy/credentials/README.md +38 -38
- package/src/auth/multi-tenancy/credentials/index.ts +6 -6
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
- package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
- package/src/auth/multi-tenancy/index.ts +17 -17
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
- package/src/auth/multi-tenancy/invitations/index.ts +37 -37
- package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
- package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
- package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
- package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
- package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
- package/src/auth/multi-tenancy/memberships/index.ts +21 -21
- package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
- package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
- package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
- package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
- package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
- package/src/auth/multi-tenancy/organizations/index.ts +23 -23
- package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
- package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
- package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
- package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
- package/src/auth/multi-tenancy/types.ts +83 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
- package/src/auth/multi-tenancy/users/index.ts +27 -27
- package/src/auth/multi-tenancy/users/server/index.ts +19 -19
- package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
- package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
- package/src/auth/multi-tenancy/users/user.ts +65 -65
- package/src/business/README.md +52 -52
- package/src/business/__tests__/entities-published.test.ts +33 -33
- package/src/business/acquisition/api-schemas.ts +759 -759
- package/src/business/acquisition/index.ts +109 -109
- package/src/business/acquisition/types.ts +402 -402
- package/src/business/base-entities.test.ts +481 -481
- package/src/business/base-entities.ts +241 -241
- package/src/business/entities-published.ts +24 -24
- package/src/business/index.ts +15 -15
- package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
- package/src/business/pdf/index.ts +10 -10
- package/src/business/pdf/server/index.ts +21 -21
- package/src/business/pdf/server/themes/default.ts +8 -8
- package/src/business/pdf/server/themes/index.ts +9 -9
- package/src/business/pdf/server/themes/types.ts +8 -8
- package/src/business/pdf/types.ts +272 -272
- package/src/business/projects/index.ts +2 -2
- package/src/business/projects/sse-events.ts +21 -21
- package/src/business/projects/types.ts +89 -89
- package/src/business/sales/api-schemas.ts +75 -75
- package/src/business/seo/__tests__/linking.test.ts +549 -549
- package/src/business/seo/__tests__/types.test.ts +404 -404
- package/src/business/seo/index.ts +2 -2
- package/src/business/seo/linking.ts +281 -281
- package/src/business/seo/types.ts +199 -199
- package/src/commands/queue/index.ts +3 -3
- package/src/commands/queue/schemas.test.ts +593 -593
- package/src/commands/queue/schemas.ts +125 -125
- package/src/commands/queue/sse-events.ts +61 -61
- package/src/commands/queue/types/action.ts +52 -52
- package/src/commands/queue/types/checkpoint.ts +44 -44
- package/src/commands/queue/types/index.ts +7 -7
- package/src/commands/queue/types/task.ts +116 -116
- package/src/commands/queue/types.ts +14 -14
- package/src/content/distribution-metadata.ts +61 -61
- package/src/content/index.ts +10 -10
- package/src/deployments/index.ts +22 -22
- package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
- package/src/execution/core/index.ts +11 -11
- package/src/execution/core/runner-types.ts +80 -80
- package/src/execution/core/server/environment.ts +31 -31
- package/src/execution/core/sse-executions.ts +119 -119
- package/src/execution/core/types.ts +29 -29
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
- package/src/execution/engine/__tests__/timeout.test.ts +565 -565
- package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
- package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
- package/src/execution/engine/agent/actions/executor.ts +205 -205
- package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
- package/src/execution/engine/agent/actions/processor.ts +116 -116
- package/src/execution/engine/agent/actions/types.ts +70 -70
- package/src/execution/engine/agent/core/agent.ts +810 -810
- package/src/execution/engine/agent/core/types.ts +155 -155
- package/src/execution/engine/agent/errors.ts +251 -251
- package/src/execution/engine/agent/index.ts +78 -78
- package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
- package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
- package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
- package/src/execution/engine/agent/memory/domains.ts +99 -99
- package/src/execution/engine/agent/memory/manager.ts +365 -365
- package/src/execution/engine/agent/memory/processor.ts +66 -66
- package/src/execution/engine/agent/memory/types.ts +90 -90
- package/src/execution/engine/agent/memory/utils.ts +134 -134
- package/src/execution/engine/agent/observability/logging.ts +467 -467
- package/src/execution/engine/agent/observability/types.ts +64 -64
- package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
- package/src/execution/engine/agent/reasoning/processor.ts +92 -92
- package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
- package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
- package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
- package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
- package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
- package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
- package/src/execution/engine/agent/reasoning/types.ts +18 -18
- package/src/execution/engine/base/errors.ts +118 -118
- package/src/execution/engine/base/index.ts +2 -2
- package/src/execution/engine/base/logging.ts +31 -31
- package/src/execution/engine/base/serialization.ts +324 -324
- package/src/execution/engine/base/types.ts +126 -126
- package/src/execution/engine/base/utils.ts +41 -41
- package/src/execution/engine/index.ts +434 -434
- package/src/execution/engine/interface/index.ts +1 -1
- package/src/execution/engine/interface/types.ts +62 -62
- package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
- package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
- package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
- package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
- package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
- package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
- package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
- package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
- package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
- package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
- package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
- package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
- package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
- package/src/execution/engine/llm/adapters/index.ts +17 -17
- package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
- package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
- package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
- package/src/execution/engine/llm/adapters/server/google.ts +283 -283
- package/src/execution/engine/llm/adapters/server/index.ts +12 -12
- package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
- package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
- package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
- package/src/execution/engine/llm/errors.ts +186 -186
- package/src/execution/engine/llm/model-info.ts +332 -332
- package/src/execution/engine/llm/response-schema-validator.ts +113 -113
- package/src/execution/engine/llm/types.ts +86 -86
- package/src/execution/engine/test-utils/index.ts +6 -6
- package/src/execution/engine/test-utils/mocks.ts +56 -56
- package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
- package/src/execution/engine/tools/integration/index.ts +53 -53
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
- package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
- package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
- package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
- package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
- package/src/execution/engine/tools/integration/server/index.ts +61 -61
- package/src/execution/engine/tools/integration/service.ts +161 -161
- package/src/execution/engine/tools/integration/tool.ts +253 -253
- package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
- package/src/execution/engine/tools/integration/types/apify.ts +92 -92
- package/src/execution/engine/tools/integration/types/index.ts +19 -19
- package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
- package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
- package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
- package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
- package/src/execution/engine/tools/lead-service-types.ts +884 -884
- package/src/execution/engine/tools/llm/index.ts +11 -11
- package/src/execution/engine/tools/llm/server/index.ts +8 -8
- package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
- package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
- package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
- package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
- package/src/execution/engine/tools/platform/email/index.ts +122 -122
- package/src/execution/engine/tools/platform/email/types.ts +96 -96
- package/src/execution/engine/tools/platform/index.ts +157 -157
- package/src/execution/engine/tools/platform/notification.ts +81 -81
- package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
- package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
- package/src/execution/engine/tools/platform/scheduler.ts +87 -87
- package/src/execution/engine/tools/platform/storage/index.ts +370 -370
- package/src/execution/engine/tools/platform/types.ts +148 -148
- package/src/execution/engine/tools/registry.ts +700 -700
- package/src/execution/engine/tools/tool-maps.ts +786 -786
- package/src/execution/engine/tools/types.ts +233 -233
- package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
- package/src/execution/engine/workflow/errors.ts +63 -63
- package/src/execution/engine/workflow/helpers/index.ts +11 -11
- package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
- package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
- package/src/execution/engine/workflow/index.ts +19 -19
- package/src/execution/engine/workflow/log-truncate.ts +26 -26
- package/src/execution/engine/workflow/logging.ts +191 -191
- package/src/execution/engine/workflow/types.ts +182 -182
- package/src/execution/engine/workflow/utils.ts +280 -280
- package/src/execution/engine/workflow/workflow.ts +168 -168
- package/src/execution/index.ts +3 -3
- package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
- package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
- package/src/execution/scheduler/api-schemas.ts +296 -296
- package/src/execution/scheduler/index.ts +50 -50
- package/src/execution/scheduler/schemas.ts +264 -264
- package/src/execution/scheduler/types.ts +111 -111
- package/src/execution/scheduler/utils.ts +364 -364
- package/src/forms/index.ts +7 -7
- package/src/forms/schemas.ts +69 -69
- package/src/forms/types.ts +70 -70
- package/src/index.ts +71 -60
- package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
- package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
- package/src/integrations/credentials/api-schemas.ts +143 -143
- package/src/integrations/credentials/index.ts +32 -32
- package/src/integrations/credentials/schemas.ts +164 -164
- package/src/integrations/credentials/utils.ts +59 -59
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
- package/src/integrations/oauth/api-schemas.ts +92 -92
- package/src/integrations/oauth/index.ts +19 -19
- package/src/integrations/oauth/provider-registry.ts +61 -61
- package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
- package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
- package/src/integrations/oauth/server/credentials.ts +39 -39
- package/src/integrations/oauth/server/refresh.ts +214 -214
- package/src/integrations/oauth/types.ts +34 -34
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
- package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
- package/src/integrations/webhook-endpoints/index.ts +28 -28
- package/src/integrations/webhook-endpoints/types.ts +51 -51
- package/src/operations/activities/api-schemas.ts +79 -79
- package/src/operations/activities/index.ts +9 -9
- package/src/operations/activities/sse-events.ts +30 -30
- package/src/operations/activities/types.ts +63 -63
- package/src/operations/debug-logs/client.ts +60 -60
- package/src/operations/debug-logs/debug-logger.ts +83 -83
- package/src/operations/debug-logs/index.ts +8 -8
- package/src/operations/debug-logs/server.ts +19 -19
- package/src/operations/debug-logs/types.ts +33 -33
- package/src/operations/index.ts +50 -50
- package/src/operations/notifications/api-schemas.ts +91 -91
- package/src/operations/notifications/index.ts +3 -3
- package/src/operations/notifications/sse-events.ts +21 -21
- package/src/operations/notifications/types.ts +47 -47
- package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
- package/src/operations/observability/__tests__/utils.test.ts +54 -54
- package/src/operations/observability/ai-usage-collector.ts +64 -64
- package/src/operations/observability/index.ts +13 -13
- package/src/operations/observability/metrics-collector.ts +49 -49
- package/src/operations/observability/schemas.ts +39 -39
- package/src/operations/observability/types.ts +463 -463
- package/src/operations/observability/utils.ts +77 -77
- package/src/operations/sessions/__tests__/manager.test.ts +821 -821
- package/src/operations/sessions/index.ts +26 -26
- package/src/operations/sessions/server/manager.ts +90 -90
- package/src/operations/sessions/server/session.ts +180 -180
- package/src/operations/sessions/types.ts +98 -98
- package/src/operations/triggers/index.ts +12 -12
- package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
- package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
- package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
- package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
- package/src/operations/triggers/webhook/index.ts +35 -35
- package/src/operations/triggers/webhook/types.ts +74 -74
- package/src/organization-model/README.md +97 -97
- package/src/organization-model/__tests__/defaults.test.ts +175 -175
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
- package/src/organization-model/__tests__/foundation.test.ts +105 -105
- package/src/organization-model/__tests__/graph.test.ts +894 -894
- package/src/organization-model/__tests__/resolve.test.ts +690 -690
- package/src/organization-model/__tests__/schema.test.ts +407 -407
- package/src/organization-model/contracts.ts +14 -14
- package/src/organization-model/defaults.ts +148 -148
- package/src/organization-model/domains/branding.ts +22 -22
- package/src/organization-model/domains/customers.ts +75 -75
- package/src/organization-model/domains/features.ts +22 -22
- package/src/organization-model/domains/goals.ts +80 -80
- package/src/organization-model/domains/identity.ts +94 -94
- package/src/organization-model/domains/navigation.ts +391 -391
- package/src/organization-model/domains/offerings.ts +66 -66
- package/src/organization-model/domains/operations.ts +85 -85
- package/src/organization-model/domains/projects.ts +48 -48
- package/src/organization-model/domains/prospecting.ts +33 -33
- package/src/organization-model/domains/roles.ts +55 -55
- package/src/organization-model/domains/sales.ts +94 -94
- package/src/organization-model/domains/shared.ts +62 -62
- package/src/organization-model/domains/statuses.ts +130 -130
- package/src/organization-model/foundation.ts +97 -97
- package/src/organization-model/graph/build.ts +399 -399
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/schema.ts +48 -48
- package/src/organization-model/graph/types.ts +40 -40
- package/src/organization-model/index.ts +13 -13
- package/src/organization-model/organization-graph.mdx +272 -272
- package/src/organization-model/organization-model.mdx +320 -320
- package/src/organization-model/published.ts +85 -85
- package/src/organization-model/resolve.ts +66 -66
- package/src/organization-model/schema.ts +287 -287
- package/src/organization-model/types.ts +46 -46
- package/src/platform/api/index.ts +1 -1
- package/src/platform/api/types.ts +35 -35
- package/src/platform/constants/http.ts +37 -37
- package/src/platform/constants/index.ts +5 -5
- package/src/platform/constants/limits.ts +32 -32
- package/src/platform/constants/resilience.ts +51 -51
- package/src/platform/constants/timeouts.ts +20 -20
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
- package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
- package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
- package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
- package/src/platform/registry/command-view.ts +180 -180
- package/src/platform/registry/domains.ts +165 -165
- package/src/platform/registry/index.ts +93 -93
- package/src/platform/registry/reserved.ts +24 -24
- package/src/platform/registry/resource-metadata.ts +59 -59
- package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
- package/src/platform/registry/resource-registry.ts +876 -876
- package/src/platform/registry/serialization.ts +273 -273
- package/src/platform/registry/serialized-types.ts +231 -231
- package/src/platform/registry/stats-types.ts +66 -66
- package/src/platform/registry/types.ts +404 -404
- package/src/platform/registry/validation.ts +513 -513
- package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
- package/src/platform/resilience/circuit-breaker.ts +164 -164
- package/src/platform/resilience/errors.ts +68 -68
- package/src/platform/resilience/http-error-mapper.ts +129 -129
- package/src/platform/resilience/index.ts +93 -93
- package/src/platform/resilience/rate-limiter-types.ts +46 -46
- package/src/platform/resilience/rate-limiter.ts +140 -140
- package/src/platform/resilience/retry.ts +89 -89
- package/src/platform/resilience/timeout.ts +63 -63
- package/src/platform/sse/events.ts +37 -37
- package/src/platform/sse/index.ts +7 -7
- package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
- package/src/platform/utils/currency.ts +96 -96
- package/src/platform/utils/debounce.ts +52 -52
- package/src/platform/utils/error.ts +41 -41
- package/src/platform/utils/hmac.test.ts +97 -97
- package/src/platform/utils/index.ts +32 -32
- package/src/platform/utils/server/betterstack-logger.ts +210 -210
- package/src/platform/utils/server/hmac.ts +44 -44
- package/src/platform/utils/server/unsubscribe.ts +111 -111
- package/src/platform/utils/token-counter.ts +96 -96
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.ts +268 -268
- package/src/published.ts +1 -1
- package/src/reference/_generated/contracts.md +607 -607
- package/src/reference/glossary.md +105 -105
- package/src/requests/__tests__/api-schemas.test.ts +277 -277
- package/src/requests/api-schemas.ts +83 -83
- package/src/requests/index.ts +1 -1
- package/src/scaffold-registry/__tests__/index.test.ts +17 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
- package/src/scaffold-registry/index.ts +205 -189
- package/src/scaffold-registry/schema.ts +196 -128
- package/src/server.ts +272 -272
- package/src/supabase/database.types.ts +2719 -2719
- package/src/supabase/helpers.ts +20 -20
- package/src/supabase/index.ts +52 -52
- package/src/supabase/server/client.ts +58 -58
- package/src/test-utils/README.md +30 -138
- package/src/test-utils/browser-mocks.ts +54 -54
- package/src/test-utils/fixtures/api-keys.ts +52 -52
- package/src/test-utils/fixtures/index.ts +4 -4
- package/src/test-utils/fixtures/memberships.ts +80 -80
- package/src/test-utils/fixtures/organizations.ts +69 -69
- package/src/test-utils/fixtures/users.ts +79 -79
- package/src/test-utils/index.ts +7 -8
- package/src/test-utils/mocks/index.ts +2 -2
- package/src/test-utils/mocks/supabase.ts +142 -142
- package/src/test-utils/mocks/workos.ts +108 -108
- package/src/test-utils/published.ts +4 -0
- package/src/test-utils/rls/RLSTestContext.ts +554 -554
- package/src/test-utils/rls/index.ts +1 -1
|
@@ -1,722 +1,722 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Adapter Tests
|
|
3
|
-
* Verifies model options handling, message translation, and configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
7
|
-
import { GoogleAdapter, isGoogleModel } from '../server/google'
|
|
8
|
-
import { LLMResponseParseError } from '../../errors'
|
|
9
|
-
import type { LLMGenerateRequest } from '../../types'
|
|
10
|
-
|
|
11
|
-
// Mock Google GenAI SDK
|
|
12
|
-
const mockGenerateContent = vi.fn()
|
|
13
|
-
|
|
14
|
-
vi.mock('@google/genai', () => ({
|
|
15
|
-
GoogleGenAI: vi.fn().mockImplementation(() => ({
|
|
16
|
-
models: {
|
|
17
|
-
generateContent: mockGenerateContent
|
|
18
|
-
}
|
|
19
|
-
}))
|
|
20
|
-
}))
|
|
21
|
-
|
|
22
|
-
describe('GoogleAdapter', () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.clearAllMocks()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const baseRequest: LLMGenerateRequest = {
|
|
28
|
-
messages: [
|
|
29
|
-
{ role: 'system', content: 'You are a helpful assistant' },
|
|
30
|
-
{ role: 'user', content: 'Hello' }
|
|
31
|
-
],
|
|
32
|
-
responseSchema: {
|
|
33
|
-
type: 'object',
|
|
34
|
-
properties: {
|
|
35
|
-
response: { type: 'string' }
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
maxOutputTokens: 1000
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const mockGoogleResponse = {
|
|
42
|
-
text: JSON.stringify({ response: 'Hello! How can I help you?' }),
|
|
43
|
-
usageMetadata: {
|
|
44
|
-
promptTokenCount: 10,
|
|
45
|
-
candidatesTokenCount: 20,
|
|
46
|
-
totalTokenCount: 30
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('isGoogleModel', () => {
|
|
51
|
-
it('returns true for gemini models with google provider', () => {
|
|
52
|
-
expect(isGoogleModel('gemini-3-flash-preview', 'google')).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns false for gemini models with other providers', () => {
|
|
56
|
-
expect(isGoogleModel('gemini-3-flash-preview', 'openrouter')).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('returns false for non-gemini models', () => {
|
|
60
|
-
expect(isGoogleModel('gpt-5', 'google')).toBe(false)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('Basic Configuration', () => {
|
|
65
|
-
it('creates adapter without model options', async () => {
|
|
66
|
-
const adapter = new GoogleAdapter({
|
|
67
|
-
apiKey: 'test-key',
|
|
68
|
-
model: 'gemini-3-flash-preview'
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
72
|
-
|
|
73
|
-
await adapter.generate(baseRequest)
|
|
74
|
-
|
|
75
|
-
// Verify call was made
|
|
76
|
-
expect(mockGenerateContent).toHaveBeenCalledTimes(1)
|
|
77
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
78
|
-
|
|
79
|
-
// Verify no thinking config when not specified
|
|
80
|
-
expect(callParams.config).not.toHaveProperty('thinkingConfig')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('creates adapter with thinkingLevel option', async () => {
|
|
84
|
-
const adapter = new GoogleAdapter({
|
|
85
|
-
apiKey: 'test-key',
|
|
86
|
-
model: 'gemini-3-flash-preview',
|
|
87
|
-
modelOptions: {
|
|
88
|
-
thinkingLevel: 'medium'
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
93
|
-
|
|
94
|
-
await adapter.generate(baseRequest)
|
|
95
|
-
|
|
96
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
97
|
-
expect(callParams.config.thinkingConfig).toEqual({
|
|
98
|
-
thinkingLevel: 'medium'
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('Thinking Level Options', () => {
|
|
104
|
-
it.each(['minimal', 'low', 'medium', 'high'] as const)('supports %s thinking level', async (level) => {
|
|
105
|
-
const adapter = new GoogleAdapter({
|
|
106
|
-
apiKey: 'test-key',
|
|
107
|
-
model: 'gemini-3-flash-preview',
|
|
108
|
-
modelOptions: {
|
|
109
|
-
thinkingLevel: level
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
114
|
-
|
|
115
|
-
await adapter.generate(baseRequest)
|
|
116
|
-
|
|
117
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
118
|
-
expect(callParams.config.thinkingConfig.thinkingLevel).toBe(level)
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe('Message Translation', () => {
|
|
123
|
-
it('converts system messages to systemInstruction', async () => {
|
|
124
|
-
const adapter = new GoogleAdapter({
|
|
125
|
-
apiKey: 'test-key',
|
|
126
|
-
model: 'gemini-3-flash-preview'
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
130
|
-
|
|
131
|
-
await adapter.generate({
|
|
132
|
-
messages: [
|
|
133
|
-
{ role: 'system', content: 'You are helpful' },
|
|
134
|
-
{ role: 'user', content: 'Hello' }
|
|
135
|
-
],
|
|
136
|
-
responseSchema: baseRequest.responseSchema,
|
|
137
|
-
maxOutputTokens: 1000
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
141
|
-
|
|
142
|
-
// System message should be in systemInstruction
|
|
143
|
-
expect(callParams.config.systemInstruction).toBe('You are helpful')
|
|
144
|
-
|
|
145
|
-
// Contents should only have user message
|
|
146
|
-
expect(callParams.contents).toHaveLength(1)
|
|
147
|
-
expect(callParams.contents[0].role).toBe('user')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('combines multiple system messages', async () => {
|
|
151
|
-
const adapter = new GoogleAdapter({
|
|
152
|
-
apiKey: 'test-key',
|
|
153
|
-
model: 'gemini-3-flash-preview'
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
157
|
-
|
|
158
|
-
await adapter.generate({
|
|
159
|
-
messages: [
|
|
160
|
-
{ role: 'system', content: 'You are helpful' },
|
|
161
|
-
{ role: 'system', content: 'Be concise' },
|
|
162
|
-
{ role: 'user', content: 'Hello' }
|
|
163
|
-
],
|
|
164
|
-
responseSchema: baseRequest.responseSchema,
|
|
165
|
-
maxOutputTokens: 1000
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
169
|
-
expect(callParams.config.systemInstruction).toBe('You are helpful\n\nBe concise')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('converts assistant role to model role', async () => {
|
|
173
|
-
const adapter = new GoogleAdapter({
|
|
174
|
-
apiKey: 'test-key',
|
|
175
|
-
model: 'gemini-3-flash-preview'
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
179
|
-
|
|
180
|
-
await adapter.generate({
|
|
181
|
-
messages: [
|
|
182
|
-
{ role: 'user', content: 'Hello' },
|
|
183
|
-
{ role: 'assistant', content: 'Hi there!' },
|
|
184
|
-
{ role: 'user', content: 'How are you?' }
|
|
185
|
-
],
|
|
186
|
-
responseSchema: baseRequest.responseSchema,
|
|
187
|
-
maxOutputTokens: 1000
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
191
|
-
expect(callParams.contents[0].role).toBe('user')
|
|
192
|
-
expect(callParams.contents[1].role).toBe('model')
|
|
193
|
-
expect(callParams.contents[2].role).toBe('user')
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
describe('Structured Output', () => {
|
|
198
|
-
it('configures JSON response format with schema', async () => {
|
|
199
|
-
const adapter = new GoogleAdapter({
|
|
200
|
-
apiKey: 'test-key',
|
|
201
|
-
model: 'gemini-3-flash-preview'
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
205
|
-
|
|
206
|
-
await adapter.generate(baseRequest)
|
|
207
|
-
|
|
208
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
209
|
-
expect(callParams.config.responseMimeType).toBe('application/json')
|
|
210
|
-
expect(callParams.config.responseSchema).toEqual(baseRequest.responseSchema)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('parses JSON response correctly', async () => {
|
|
214
|
-
const adapter = new GoogleAdapter({
|
|
215
|
-
apiKey: 'test-key',
|
|
216
|
-
model: 'gemini-3-flash-preview'
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
220
|
-
text: JSON.stringify({ answer: 'Paris', confidence: 0.95 }),
|
|
221
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const response = await adapter.generate<{ answer: string; confidence: number }>({
|
|
225
|
-
messages: [{ role: 'user', content: 'What is the capital of France?' }],
|
|
226
|
-
responseSchema: {
|
|
227
|
-
type: 'object',
|
|
228
|
-
properties: {
|
|
229
|
-
answer: { type: 'string' },
|
|
230
|
-
confidence: { type: 'number' }
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
maxOutputTokens: 500
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
expect(response.output.answer).toBe('Paris')
|
|
237
|
-
expect(response.output.confidence).toBe(0.95)
|
|
238
|
-
})
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
describe('Usage Metadata', () => {
|
|
242
|
-
it('extracts token usage from response', async () => {
|
|
243
|
-
const adapter = new GoogleAdapter({
|
|
244
|
-
apiKey: 'test-key',
|
|
245
|
-
model: 'gemini-3-flash-preview'
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
249
|
-
|
|
250
|
-
const response = await adapter.generate(baseRequest)
|
|
251
|
-
|
|
252
|
-
expect(response.usage).toEqual({
|
|
253
|
-
inputTokens: 10,
|
|
254
|
-
outputTokens: 20,
|
|
255
|
-
totalTokens: 30
|
|
256
|
-
})
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('handles missing usage metadata gracefully', async () => {
|
|
260
|
-
const adapter = new GoogleAdapter({
|
|
261
|
-
apiKey: 'test-key',
|
|
262
|
-
model: 'gemini-3-flash-preview'
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
266
|
-
text: JSON.stringify({ response: 'Hello!' })
|
|
267
|
-
// No usageMetadata
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
const response = await adapter.generate(baseRequest)
|
|
271
|
-
|
|
272
|
-
expect(response.usage).toBeUndefined()
|
|
273
|
-
})
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
describe('MaxTokens Configuration', () => {
|
|
277
|
-
it('passes maxOutputTokens to Google API', async () => {
|
|
278
|
-
const adapter = new GoogleAdapter({
|
|
279
|
-
apiKey: 'test-key',
|
|
280
|
-
model: 'gemini-3-flash-preview'
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
284
|
-
|
|
285
|
-
await adapter.generate({
|
|
286
|
-
...baseRequest,
|
|
287
|
-
maxOutputTokens: 2000
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
291
|
-
expect(callParams.config.maxOutputTokens).toBe(2000)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('omits maxOutputTokens when not specified', async () => {
|
|
295
|
-
const adapter = new GoogleAdapter({
|
|
296
|
-
apiKey: 'test-key',
|
|
297
|
-
model: 'gemini-3-flash-preview'
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
301
|
-
|
|
302
|
-
await adapter.generate({
|
|
303
|
-
messages: baseRequest.messages,
|
|
304
|
-
responseSchema: baseRequest.responseSchema
|
|
305
|
-
// No maxOutputTokens
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
309
|
-
expect(callParams.config.maxOutputTokens).toBeUndefined()
|
|
310
|
-
})
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
describe('Backward Compatibility', () => {
|
|
314
|
-
it('works with existing code that does not provide model options', async () => {
|
|
315
|
-
const adapter = new GoogleAdapter({
|
|
316
|
-
apiKey: 'test-key',
|
|
317
|
-
model: 'gemini-3-flash-preview'
|
|
318
|
-
// No modelOptions field
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
322
|
-
|
|
323
|
-
const response = await adapter.generate(baseRequest)
|
|
324
|
-
|
|
325
|
-
expect(response.output).toEqual({ response: 'Hello! How can I help you?' })
|
|
326
|
-
expect(response.usage).toEqual({
|
|
327
|
-
inputTokens: 10,
|
|
328
|
-
outputTokens: 20,
|
|
329
|
-
totalTokens: 30
|
|
330
|
-
})
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('handles empty model options object', async () => {
|
|
334
|
-
const adapter = new GoogleAdapter({
|
|
335
|
-
apiKey: 'test-key',
|
|
336
|
-
model: 'gemini-3-flash-preview',
|
|
337
|
-
modelOptions: {}
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
341
|
-
|
|
342
|
-
await adapter.generate(baseRequest)
|
|
343
|
-
|
|
344
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
345
|
-
expect(callParams.config).not.toHaveProperty('thinkingConfig')
|
|
346
|
-
})
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
describe('Multiple Calls', () => {
|
|
350
|
-
it('applies same model options to multiple generate calls', async () => {
|
|
351
|
-
const adapter = new GoogleAdapter({
|
|
352
|
-
apiKey: 'test-key',
|
|
353
|
-
model: 'gemini-3-flash-preview',
|
|
354
|
-
modelOptions: {
|
|
355
|
-
thinkingLevel: 'high'
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
mockGenerateContent.mockResolvedValue(mockGoogleResponse)
|
|
360
|
-
|
|
361
|
-
// First call
|
|
362
|
-
await adapter.generate(baseRequest)
|
|
363
|
-
expect(mockGenerateContent.mock.calls[0][0].config.thinkingConfig.thinkingLevel).toBe('high')
|
|
364
|
-
|
|
365
|
-
// Second call
|
|
366
|
-
await adapter.generate(baseRequest)
|
|
367
|
-
expect(mockGenerateContent.mock.calls[1][0].config.thinkingConfig.thinkingLevel).toBe('high')
|
|
368
|
-
})
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
describe('Error Handling', () => {
|
|
372
|
-
it('propagates API errors', async () => {
|
|
373
|
-
const adapter = new GoogleAdapter({
|
|
374
|
-
apiKey: 'test-key',
|
|
375
|
-
model: 'gemini-3-flash-preview'
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
const apiError = new Error('API rate limit exceeded')
|
|
379
|
-
mockGenerateContent.mockRejectedValueOnce(apiError)
|
|
380
|
-
|
|
381
|
-
await expect(adapter.generate(baseRequest)).rejects.toThrow('API rate limit exceeded')
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
it('throws LLMResponseParseError for empty response text with diagnostic details', async () => {
|
|
385
|
-
const adapter = new GoogleAdapter({
|
|
386
|
-
apiKey: 'test-key',
|
|
387
|
-
model: 'gemini-3-flash-preview'
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
391
|
-
text: '',
|
|
392
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
await expect(adapter.generate(baseRequest)).rejects.toThrow(
|
|
396
|
-
expect.objectContaining({
|
|
397
|
-
message: expect.stringContaining('Google Gemini returned no output')
|
|
398
|
-
})
|
|
399
|
-
)
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('includes block reason when prompt is blocked', async () => {
|
|
403
|
-
const adapter = new GoogleAdapter({
|
|
404
|
-
apiKey: 'test-key',
|
|
405
|
-
model: 'gemini-3-flash-preview'
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
409
|
-
text: undefined,
|
|
410
|
-
promptFeedback: { blockReason: 'SAFETY' },
|
|
411
|
-
candidates: [],
|
|
412
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
await expect(adapter.generate(baseRequest)).rejects.toThrow(/prompt blocked: SAFETY/)
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('includes finish reason when candidate is not STOP', async () => {
|
|
419
|
-
const adapter = new GoogleAdapter({
|
|
420
|
-
apiKey: 'test-key',
|
|
421
|
-
model: 'gemini-3-flash-preview'
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
425
|
-
text: undefined,
|
|
426
|
-
candidates: [{ finishReason: 'SAFETY', content: { parts: [] } }],
|
|
427
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
await expect(adapter.generate(baseRequest)).rejects.toThrow(/finish reason: SAFETY/)
|
|
431
|
-
})
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
describe('JSON Parse Error Handling', () => {
|
|
435
|
-
it('throws LLMResponseParseError when response is invalid JSON', async () => {
|
|
436
|
-
const adapter = new GoogleAdapter({
|
|
437
|
-
apiKey: 'test-key',
|
|
438
|
-
model: 'gemini-3-flash-preview'
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
442
|
-
text: '{"response": "incomplete json', // Invalid JSON - unterminated string
|
|
443
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
await expect(adapter.generate(baseRequest)).rejects.toThrow(LLMResponseParseError)
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
it('includes truncated raw content in LLMResponseParseError context', async () => {
|
|
450
|
-
const adapter = new GoogleAdapter({
|
|
451
|
-
apiKey: 'test-key',
|
|
452
|
-
model: 'gemini-3-flash-preview'
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
mockGenerateContent.mockResolvedValueOnce({
|
|
456
|
-
text: '{"response": "incomplete',
|
|
457
|
-
usageMetadata: mockGoogleResponse.usageMetadata
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
try {
|
|
461
|
-
await adapter.generate(baseRequest)
|
|
462
|
-
expect.fail('Should have thrown')
|
|
463
|
-
} catch (error) {
|
|
464
|
-
expect(error).toBeInstanceOf(LLMResponseParseError)
|
|
465
|
-
const parseError = error as LLMResponseParseError
|
|
466
|
-
expect(parseError.context).toHaveProperty('rawContent')
|
|
467
|
-
expect(parseError.context).toHaveProperty('parseError')
|
|
468
|
-
expect(parseError.message).toContain('Failed to parse LLM response as JSON')
|
|
469
|
-
}
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('parses valid JSON successfully', async () => {
|
|
473
|
-
const adapter = new GoogleAdapter({
|
|
474
|
-
apiKey: 'test-key',
|
|
475
|
-
model: 'gemini-3-flash-preview'
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
479
|
-
|
|
480
|
-
const result = await adapter.generate(baseRequest)
|
|
481
|
-
expect(result.output).toEqual({ response: 'Hello! How can I help you?' })
|
|
482
|
-
})
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
describe('Schema Transformation for Gemini', () => {
|
|
486
|
-
it('transforms const to enum with type string in responseSchema', async () => {
|
|
487
|
-
const adapter = new GoogleAdapter({
|
|
488
|
-
apiKey: 'test-key',
|
|
489
|
-
model: 'gemini-3-flash-preview'
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
493
|
-
|
|
494
|
-
// Schema with const (from z.literal())
|
|
495
|
-
await adapter.generate({
|
|
496
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
497
|
-
responseSchema: {
|
|
498
|
-
type: 'object',
|
|
499
|
-
properties: {
|
|
500
|
-
type: { const: 'tool-call' }, // const should become type: "string", enum: ["tool-call"]
|
|
501
|
-
name: { type: 'string' }
|
|
502
|
-
}
|
|
503
|
-
},
|
|
504
|
-
maxOutputTokens: 1000
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
508
|
-
|
|
509
|
-
// Verify const was transformed to type + enum (Gemini requires type: "string" for enum)
|
|
510
|
-
expect(callParams.config.responseSchema.properties.type).toEqual({ type: 'string', enum: ['tool-call'] })
|
|
511
|
-
expect(callParams.config.responseSchema.properties.type).not.toHaveProperty('const')
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
it('transforms nested const in anyOf schema', async () => {
|
|
515
|
-
const adapter = new GoogleAdapter({
|
|
516
|
-
apiKey: 'test-key',
|
|
517
|
-
model: 'gemini-3-flash-preview'
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
521
|
-
|
|
522
|
-
// Schema with anyOf containing const (discriminated union pattern)
|
|
523
|
-
await adapter.generate({
|
|
524
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
525
|
-
responseSchema: {
|
|
526
|
-
type: 'object',
|
|
527
|
-
properties: {
|
|
528
|
-
action: {
|
|
529
|
-
anyOf: [
|
|
530
|
-
{ type: 'object', properties: { type: { const: 'complete' } } },
|
|
531
|
-
{ type: 'object', properties: { type: { const: 'tool-call' }, name: { type: 'string' } } }
|
|
532
|
-
]
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
},
|
|
536
|
-
maxOutputTokens: 1000
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
540
|
-
const anyOf = callParams.config.responseSchema.properties.action.anyOf
|
|
541
|
-
|
|
542
|
-
// Verify both const values were transformed to type + enum
|
|
543
|
-
expect(anyOf[0].properties.type).toEqual({ type: 'string', enum: ['complete'] })
|
|
544
|
-
expect(anyOf[1].properties.type).toEqual({ type: 'string', enum: ['tool-call'] })
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
it('removes $schema property', async () => {
|
|
548
|
-
const adapter = new GoogleAdapter({
|
|
549
|
-
apiKey: 'test-key',
|
|
550
|
-
model: 'gemini-3-flash-preview'
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
554
|
-
|
|
555
|
-
await adapter.generate({
|
|
556
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
557
|
-
responseSchema: {
|
|
558
|
-
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
559
|
-
type: 'object',
|
|
560
|
-
properties: {
|
|
561
|
-
name: { type: 'string' }
|
|
562
|
-
}
|
|
563
|
-
},
|
|
564
|
-
maxOutputTokens: 1000
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
568
|
-
|
|
569
|
-
// Verify $schema was removed
|
|
570
|
-
expect(callParams.config.responseSchema).not.toHaveProperty('$schema')
|
|
571
|
-
expect(callParams.config.responseSchema.type).toBe('object')
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
it('inlines $ref definitions from $defs', async () => {
|
|
575
|
-
const adapter = new GoogleAdapter({
|
|
576
|
-
apiKey: 'test-key',
|
|
577
|
-
model: 'gemini-3-flash-preview'
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
581
|
-
|
|
582
|
-
// Schema with $ref and $defs
|
|
583
|
-
await adapter.generate({
|
|
584
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
585
|
-
responseSchema: {
|
|
586
|
-
type: 'object',
|
|
587
|
-
properties: {
|
|
588
|
-
item: { $ref: '#/$defs/Item' }
|
|
589
|
-
},
|
|
590
|
-
$defs: {
|
|
591
|
-
Item: {
|
|
592
|
-
type: 'object',
|
|
593
|
-
properties: {
|
|
594
|
-
id: { type: 'string' },
|
|
595
|
-
value: { type: 'number' }
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
maxOutputTokens: 1000
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
604
|
-
|
|
605
|
-
// Verify $ref was inlined
|
|
606
|
-
expect(callParams.config.responseSchema.properties.item).toEqual({
|
|
607
|
-
type: 'object',
|
|
608
|
-
properties: {
|
|
609
|
-
id: { type: 'string' },
|
|
610
|
-
value: { type: 'number' }
|
|
611
|
-
}
|
|
612
|
-
})
|
|
613
|
-
// Verify $defs was removed
|
|
614
|
-
expect(callParams.config.responseSchema).not.toHaveProperty('$defs')
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
it('handles deeply nested schemas with const', async () => {
|
|
618
|
-
const adapter = new GoogleAdapter({
|
|
619
|
-
apiKey: 'test-key',
|
|
620
|
-
model: 'gemini-3-flash-preview'
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
624
|
-
|
|
625
|
-
await adapter.generate({
|
|
626
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
627
|
-
responseSchema: {
|
|
628
|
-
type: 'object',
|
|
629
|
-
properties: {
|
|
630
|
-
data: {
|
|
631
|
-
type: 'array',
|
|
632
|
-
items: {
|
|
633
|
-
type: 'object',
|
|
634
|
-
properties: {
|
|
635
|
-
status: { const: 'active' }
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
maxOutputTokens: 1000
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
645
|
-
|
|
646
|
-
// Verify nested const was transformed to type + enum
|
|
647
|
-
expect(callParams.config.responseSchema.properties.data.items.properties.status).toEqual({
|
|
648
|
-
type: 'string',
|
|
649
|
-
enum: ['active']
|
|
650
|
-
})
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
it('preserves other schema properties during transformation', async () => {
|
|
654
|
-
const adapter = new GoogleAdapter({
|
|
655
|
-
apiKey: 'test-key',
|
|
656
|
-
model: 'gemini-3-flash-preview'
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
660
|
-
|
|
661
|
-
await adapter.generate({
|
|
662
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
663
|
-
responseSchema: {
|
|
664
|
-
type: 'object',
|
|
665
|
-
properties: {
|
|
666
|
-
name: { type: 'string', description: 'User name', minLength: 1 },
|
|
667
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
668
|
-
role: { const: 'admin' }
|
|
669
|
-
},
|
|
670
|
-
required: ['name', 'role'],
|
|
671
|
-
additionalProperties: false
|
|
672
|
-
},
|
|
673
|
-
maxOutputTokens: 1000
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
677
|
-
const schema = callParams.config.responseSchema
|
|
678
|
-
|
|
679
|
-
// Verify structure preserved
|
|
680
|
-
expect(schema.type).toBe('object')
|
|
681
|
-
expect(schema.required).toEqual(['name', 'role'])
|
|
682
|
-
expect(schema.additionalProperties).toBe(false)
|
|
683
|
-
|
|
684
|
-
// Verify property details preserved
|
|
685
|
-
expect(schema.properties.name).toEqual({ type: 'string', description: 'User name', minLength: 1 })
|
|
686
|
-
expect(schema.properties.age).toEqual({ type: 'integer', minimum: 0, maximum: 150 })
|
|
687
|
-
|
|
688
|
-
// Verify const was transformed to type + enum
|
|
689
|
-
expect(schema.properties.role).toEqual({ type: 'string', enum: ['admin'] })
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
it('adds placeholder property to empty object schemas', async () => {
|
|
693
|
-
const adapter = new GoogleAdapter({
|
|
694
|
-
apiKey: 'test-key',
|
|
695
|
-
model: 'gemini-3-flash-preview'
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
699
|
-
|
|
700
|
-
await adapter.generate({
|
|
701
|
-
messages: [{ role: 'user', content: 'Test' }],
|
|
702
|
-
responseSchema: {
|
|
703
|
-
type: 'object',
|
|
704
|
-
properties: {
|
|
705
|
-
data: {
|
|
706
|
-
type: 'object',
|
|
707
|
-
properties: {} // Empty properties - Gemini rejects this
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
},
|
|
711
|
-
maxOutputTokens: 1000
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
715
|
-
|
|
716
|
-
// Verify placeholder was added to empty object
|
|
717
|
-
// Uses 'placeholderField' - a name Google accepts (no underscore prefix)
|
|
718
|
-
expect(callParams.config.responseSchema.properties.data.properties).toHaveProperty('placeholderField')
|
|
719
|
-
expect(callParams.config.responseSchema.properties.data.properties.placeholderField.type).toBe('string')
|
|
720
|
-
})
|
|
721
|
-
})
|
|
722
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Google Adapter Tests
|
|
3
|
+
* Verifies model options handling, message translation, and configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
7
|
+
import { GoogleAdapter, isGoogleModel } from '../server/google'
|
|
8
|
+
import { LLMResponseParseError } from '../../errors'
|
|
9
|
+
import type { LLMGenerateRequest } from '../../types'
|
|
10
|
+
|
|
11
|
+
// Mock Google GenAI SDK
|
|
12
|
+
const mockGenerateContent = vi.fn()
|
|
13
|
+
|
|
14
|
+
vi.mock('@google/genai', () => ({
|
|
15
|
+
GoogleGenAI: vi.fn().mockImplementation(() => ({
|
|
16
|
+
models: {
|
|
17
|
+
generateContent: mockGenerateContent
|
|
18
|
+
}
|
|
19
|
+
}))
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
describe('GoogleAdapter', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const baseRequest: LLMGenerateRequest = {
|
|
28
|
+
messages: [
|
|
29
|
+
{ role: 'system', content: 'You are a helpful assistant' },
|
|
30
|
+
{ role: 'user', content: 'Hello' }
|
|
31
|
+
],
|
|
32
|
+
responseSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
response: { type: 'string' }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
maxOutputTokens: 1000
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mockGoogleResponse = {
|
|
42
|
+
text: JSON.stringify({ response: 'Hello! How can I help you?' }),
|
|
43
|
+
usageMetadata: {
|
|
44
|
+
promptTokenCount: 10,
|
|
45
|
+
candidatesTokenCount: 20,
|
|
46
|
+
totalTokenCount: 30
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('isGoogleModel', () => {
|
|
51
|
+
it('returns true for gemini models with google provider', () => {
|
|
52
|
+
expect(isGoogleModel('gemini-3-flash-preview', 'google')).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns false for gemini models with other providers', () => {
|
|
56
|
+
expect(isGoogleModel('gemini-3-flash-preview', 'openrouter')).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns false for non-gemini models', () => {
|
|
60
|
+
expect(isGoogleModel('gpt-5', 'google')).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('Basic Configuration', () => {
|
|
65
|
+
it('creates adapter without model options', async () => {
|
|
66
|
+
const adapter = new GoogleAdapter({
|
|
67
|
+
apiKey: 'test-key',
|
|
68
|
+
model: 'gemini-3-flash-preview'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
72
|
+
|
|
73
|
+
await adapter.generate(baseRequest)
|
|
74
|
+
|
|
75
|
+
// Verify call was made
|
|
76
|
+
expect(mockGenerateContent).toHaveBeenCalledTimes(1)
|
|
77
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
78
|
+
|
|
79
|
+
// Verify no thinking config when not specified
|
|
80
|
+
expect(callParams.config).not.toHaveProperty('thinkingConfig')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('creates adapter with thinkingLevel option', async () => {
|
|
84
|
+
const adapter = new GoogleAdapter({
|
|
85
|
+
apiKey: 'test-key',
|
|
86
|
+
model: 'gemini-3-flash-preview',
|
|
87
|
+
modelOptions: {
|
|
88
|
+
thinkingLevel: 'medium'
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
93
|
+
|
|
94
|
+
await adapter.generate(baseRequest)
|
|
95
|
+
|
|
96
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
97
|
+
expect(callParams.config.thinkingConfig).toEqual({
|
|
98
|
+
thinkingLevel: 'medium'
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('Thinking Level Options', () => {
|
|
104
|
+
it.each(['minimal', 'low', 'medium', 'high'] as const)('supports %s thinking level', async (level) => {
|
|
105
|
+
const adapter = new GoogleAdapter({
|
|
106
|
+
apiKey: 'test-key',
|
|
107
|
+
model: 'gemini-3-flash-preview',
|
|
108
|
+
modelOptions: {
|
|
109
|
+
thinkingLevel: level
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
114
|
+
|
|
115
|
+
await adapter.generate(baseRequest)
|
|
116
|
+
|
|
117
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
118
|
+
expect(callParams.config.thinkingConfig.thinkingLevel).toBe(level)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('Message Translation', () => {
|
|
123
|
+
it('converts system messages to systemInstruction', async () => {
|
|
124
|
+
const adapter = new GoogleAdapter({
|
|
125
|
+
apiKey: 'test-key',
|
|
126
|
+
model: 'gemini-3-flash-preview'
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
130
|
+
|
|
131
|
+
await adapter.generate({
|
|
132
|
+
messages: [
|
|
133
|
+
{ role: 'system', content: 'You are helpful' },
|
|
134
|
+
{ role: 'user', content: 'Hello' }
|
|
135
|
+
],
|
|
136
|
+
responseSchema: baseRequest.responseSchema,
|
|
137
|
+
maxOutputTokens: 1000
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
141
|
+
|
|
142
|
+
// System message should be in systemInstruction
|
|
143
|
+
expect(callParams.config.systemInstruction).toBe('You are helpful')
|
|
144
|
+
|
|
145
|
+
// Contents should only have user message
|
|
146
|
+
expect(callParams.contents).toHaveLength(1)
|
|
147
|
+
expect(callParams.contents[0].role).toBe('user')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('combines multiple system messages', async () => {
|
|
151
|
+
const adapter = new GoogleAdapter({
|
|
152
|
+
apiKey: 'test-key',
|
|
153
|
+
model: 'gemini-3-flash-preview'
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
157
|
+
|
|
158
|
+
await adapter.generate({
|
|
159
|
+
messages: [
|
|
160
|
+
{ role: 'system', content: 'You are helpful' },
|
|
161
|
+
{ role: 'system', content: 'Be concise' },
|
|
162
|
+
{ role: 'user', content: 'Hello' }
|
|
163
|
+
],
|
|
164
|
+
responseSchema: baseRequest.responseSchema,
|
|
165
|
+
maxOutputTokens: 1000
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
169
|
+
expect(callParams.config.systemInstruction).toBe('You are helpful\n\nBe concise')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('converts assistant role to model role', async () => {
|
|
173
|
+
const adapter = new GoogleAdapter({
|
|
174
|
+
apiKey: 'test-key',
|
|
175
|
+
model: 'gemini-3-flash-preview'
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
179
|
+
|
|
180
|
+
await adapter.generate({
|
|
181
|
+
messages: [
|
|
182
|
+
{ role: 'user', content: 'Hello' },
|
|
183
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
184
|
+
{ role: 'user', content: 'How are you?' }
|
|
185
|
+
],
|
|
186
|
+
responseSchema: baseRequest.responseSchema,
|
|
187
|
+
maxOutputTokens: 1000
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
191
|
+
expect(callParams.contents[0].role).toBe('user')
|
|
192
|
+
expect(callParams.contents[1].role).toBe('model')
|
|
193
|
+
expect(callParams.contents[2].role).toBe('user')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('Structured Output', () => {
|
|
198
|
+
it('configures JSON response format with schema', async () => {
|
|
199
|
+
const adapter = new GoogleAdapter({
|
|
200
|
+
apiKey: 'test-key',
|
|
201
|
+
model: 'gemini-3-flash-preview'
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
205
|
+
|
|
206
|
+
await adapter.generate(baseRequest)
|
|
207
|
+
|
|
208
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
209
|
+
expect(callParams.config.responseMimeType).toBe('application/json')
|
|
210
|
+
expect(callParams.config.responseSchema).toEqual(baseRequest.responseSchema)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('parses JSON response correctly', async () => {
|
|
214
|
+
const adapter = new GoogleAdapter({
|
|
215
|
+
apiKey: 'test-key',
|
|
216
|
+
model: 'gemini-3-flash-preview'
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
220
|
+
text: JSON.stringify({ answer: 'Paris', confidence: 0.95 }),
|
|
221
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const response = await adapter.generate<{ answer: string; confidence: number }>({
|
|
225
|
+
messages: [{ role: 'user', content: 'What is the capital of France?' }],
|
|
226
|
+
responseSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
answer: { type: 'string' },
|
|
230
|
+
confidence: { type: 'number' }
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
maxOutputTokens: 500
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect(response.output.answer).toBe('Paris')
|
|
237
|
+
expect(response.output.confidence).toBe(0.95)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('Usage Metadata', () => {
|
|
242
|
+
it('extracts token usage from response', async () => {
|
|
243
|
+
const adapter = new GoogleAdapter({
|
|
244
|
+
apiKey: 'test-key',
|
|
245
|
+
model: 'gemini-3-flash-preview'
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
249
|
+
|
|
250
|
+
const response = await adapter.generate(baseRequest)
|
|
251
|
+
|
|
252
|
+
expect(response.usage).toEqual({
|
|
253
|
+
inputTokens: 10,
|
|
254
|
+
outputTokens: 20,
|
|
255
|
+
totalTokens: 30
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('handles missing usage metadata gracefully', async () => {
|
|
260
|
+
const adapter = new GoogleAdapter({
|
|
261
|
+
apiKey: 'test-key',
|
|
262
|
+
model: 'gemini-3-flash-preview'
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
266
|
+
text: JSON.stringify({ response: 'Hello!' })
|
|
267
|
+
// No usageMetadata
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const response = await adapter.generate(baseRequest)
|
|
271
|
+
|
|
272
|
+
expect(response.usage).toBeUndefined()
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
describe('MaxTokens Configuration', () => {
|
|
277
|
+
it('passes maxOutputTokens to Google API', async () => {
|
|
278
|
+
const adapter = new GoogleAdapter({
|
|
279
|
+
apiKey: 'test-key',
|
|
280
|
+
model: 'gemini-3-flash-preview'
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
284
|
+
|
|
285
|
+
await adapter.generate({
|
|
286
|
+
...baseRequest,
|
|
287
|
+
maxOutputTokens: 2000
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
291
|
+
expect(callParams.config.maxOutputTokens).toBe(2000)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('omits maxOutputTokens when not specified', async () => {
|
|
295
|
+
const adapter = new GoogleAdapter({
|
|
296
|
+
apiKey: 'test-key',
|
|
297
|
+
model: 'gemini-3-flash-preview'
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
301
|
+
|
|
302
|
+
await adapter.generate({
|
|
303
|
+
messages: baseRequest.messages,
|
|
304
|
+
responseSchema: baseRequest.responseSchema
|
|
305
|
+
// No maxOutputTokens
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
309
|
+
expect(callParams.config.maxOutputTokens).toBeUndefined()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('Backward Compatibility', () => {
|
|
314
|
+
it('works with existing code that does not provide model options', async () => {
|
|
315
|
+
const adapter = new GoogleAdapter({
|
|
316
|
+
apiKey: 'test-key',
|
|
317
|
+
model: 'gemini-3-flash-preview'
|
|
318
|
+
// No modelOptions field
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
322
|
+
|
|
323
|
+
const response = await adapter.generate(baseRequest)
|
|
324
|
+
|
|
325
|
+
expect(response.output).toEqual({ response: 'Hello! How can I help you?' })
|
|
326
|
+
expect(response.usage).toEqual({
|
|
327
|
+
inputTokens: 10,
|
|
328
|
+
outputTokens: 20,
|
|
329
|
+
totalTokens: 30
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('handles empty model options object', async () => {
|
|
334
|
+
const adapter = new GoogleAdapter({
|
|
335
|
+
apiKey: 'test-key',
|
|
336
|
+
model: 'gemini-3-flash-preview',
|
|
337
|
+
modelOptions: {}
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
341
|
+
|
|
342
|
+
await adapter.generate(baseRequest)
|
|
343
|
+
|
|
344
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
345
|
+
expect(callParams.config).not.toHaveProperty('thinkingConfig')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('Multiple Calls', () => {
|
|
350
|
+
it('applies same model options to multiple generate calls', async () => {
|
|
351
|
+
const adapter = new GoogleAdapter({
|
|
352
|
+
apiKey: 'test-key',
|
|
353
|
+
model: 'gemini-3-flash-preview',
|
|
354
|
+
modelOptions: {
|
|
355
|
+
thinkingLevel: 'high'
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
mockGenerateContent.mockResolvedValue(mockGoogleResponse)
|
|
360
|
+
|
|
361
|
+
// First call
|
|
362
|
+
await adapter.generate(baseRequest)
|
|
363
|
+
expect(mockGenerateContent.mock.calls[0][0].config.thinkingConfig.thinkingLevel).toBe('high')
|
|
364
|
+
|
|
365
|
+
// Second call
|
|
366
|
+
await adapter.generate(baseRequest)
|
|
367
|
+
expect(mockGenerateContent.mock.calls[1][0].config.thinkingConfig.thinkingLevel).toBe('high')
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('Error Handling', () => {
|
|
372
|
+
it('propagates API errors', async () => {
|
|
373
|
+
const adapter = new GoogleAdapter({
|
|
374
|
+
apiKey: 'test-key',
|
|
375
|
+
model: 'gemini-3-flash-preview'
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const apiError = new Error('API rate limit exceeded')
|
|
379
|
+
mockGenerateContent.mockRejectedValueOnce(apiError)
|
|
380
|
+
|
|
381
|
+
await expect(adapter.generate(baseRequest)).rejects.toThrow('API rate limit exceeded')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('throws LLMResponseParseError for empty response text with diagnostic details', async () => {
|
|
385
|
+
const adapter = new GoogleAdapter({
|
|
386
|
+
apiKey: 'test-key',
|
|
387
|
+
model: 'gemini-3-flash-preview'
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
391
|
+
text: '',
|
|
392
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
await expect(adapter.generate(baseRequest)).rejects.toThrow(
|
|
396
|
+
expect.objectContaining({
|
|
397
|
+
message: expect.stringContaining('Google Gemini returned no output')
|
|
398
|
+
})
|
|
399
|
+
)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('includes block reason when prompt is blocked', async () => {
|
|
403
|
+
const adapter = new GoogleAdapter({
|
|
404
|
+
apiKey: 'test-key',
|
|
405
|
+
model: 'gemini-3-flash-preview'
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
409
|
+
text: undefined,
|
|
410
|
+
promptFeedback: { blockReason: 'SAFETY' },
|
|
411
|
+
candidates: [],
|
|
412
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
await expect(adapter.generate(baseRequest)).rejects.toThrow(/prompt blocked: SAFETY/)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('includes finish reason when candidate is not STOP', async () => {
|
|
419
|
+
const adapter = new GoogleAdapter({
|
|
420
|
+
apiKey: 'test-key',
|
|
421
|
+
model: 'gemini-3-flash-preview'
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
425
|
+
text: undefined,
|
|
426
|
+
candidates: [{ finishReason: 'SAFETY', content: { parts: [] } }],
|
|
427
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
await expect(adapter.generate(baseRequest)).rejects.toThrow(/finish reason: SAFETY/)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('JSON Parse Error Handling', () => {
|
|
435
|
+
it('throws LLMResponseParseError when response is invalid JSON', async () => {
|
|
436
|
+
const adapter = new GoogleAdapter({
|
|
437
|
+
apiKey: 'test-key',
|
|
438
|
+
model: 'gemini-3-flash-preview'
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
442
|
+
text: '{"response": "incomplete json', // Invalid JSON - unterminated string
|
|
443
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
await expect(adapter.generate(baseRequest)).rejects.toThrow(LLMResponseParseError)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('includes truncated raw content in LLMResponseParseError context', async () => {
|
|
450
|
+
const adapter = new GoogleAdapter({
|
|
451
|
+
apiKey: 'test-key',
|
|
452
|
+
model: 'gemini-3-flash-preview'
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
mockGenerateContent.mockResolvedValueOnce({
|
|
456
|
+
text: '{"response": "incomplete',
|
|
457
|
+
usageMetadata: mockGoogleResponse.usageMetadata
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
await adapter.generate(baseRequest)
|
|
462
|
+
expect.fail('Should have thrown')
|
|
463
|
+
} catch (error) {
|
|
464
|
+
expect(error).toBeInstanceOf(LLMResponseParseError)
|
|
465
|
+
const parseError = error as LLMResponseParseError
|
|
466
|
+
expect(parseError.context).toHaveProperty('rawContent')
|
|
467
|
+
expect(parseError.context).toHaveProperty('parseError')
|
|
468
|
+
expect(parseError.message).toContain('Failed to parse LLM response as JSON')
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('parses valid JSON successfully', async () => {
|
|
473
|
+
const adapter = new GoogleAdapter({
|
|
474
|
+
apiKey: 'test-key',
|
|
475
|
+
model: 'gemini-3-flash-preview'
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
479
|
+
|
|
480
|
+
const result = await adapter.generate(baseRequest)
|
|
481
|
+
expect(result.output).toEqual({ response: 'Hello! How can I help you?' })
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('Schema Transformation for Gemini', () => {
|
|
486
|
+
it('transforms const to enum with type string in responseSchema', async () => {
|
|
487
|
+
const adapter = new GoogleAdapter({
|
|
488
|
+
apiKey: 'test-key',
|
|
489
|
+
model: 'gemini-3-flash-preview'
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
493
|
+
|
|
494
|
+
// Schema with const (from z.literal())
|
|
495
|
+
await adapter.generate({
|
|
496
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
497
|
+
responseSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
type: { const: 'tool-call' }, // const should become type: "string", enum: ["tool-call"]
|
|
501
|
+
name: { type: 'string' }
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
maxOutputTokens: 1000
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
508
|
+
|
|
509
|
+
// Verify const was transformed to type + enum (Gemini requires type: "string" for enum)
|
|
510
|
+
expect(callParams.config.responseSchema.properties.type).toEqual({ type: 'string', enum: ['tool-call'] })
|
|
511
|
+
expect(callParams.config.responseSchema.properties.type).not.toHaveProperty('const')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('transforms nested const in anyOf schema', async () => {
|
|
515
|
+
const adapter = new GoogleAdapter({
|
|
516
|
+
apiKey: 'test-key',
|
|
517
|
+
model: 'gemini-3-flash-preview'
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
521
|
+
|
|
522
|
+
// Schema with anyOf containing const (discriminated union pattern)
|
|
523
|
+
await adapter.generate({
|
|
524
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
525
|
+
responseSchema: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
action: {
|
|
529
|
+
anyOf: [
|
|
530
|
+
{ type: 'object', properties: { type: { const: 'complete' } } },
|
|
531
|
+
{ type: 'object', properties: { type: { const: 'tool-call' }, name: { type: 'string' } } }
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
maxOutputTokens: 1000
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
540
|
+
const anyOf = callParams.config.responseSchema.properties.action.anyOf
|
|
541
|
+
|
|
542
|
+
// Verify both const values were transformed to type + enum
|
|
543
|
+
expect(anyOf[0].properties.type).toEqual({ type: 'string', enum: ['complete'] })
|
|
544
|
+
expect(anyOf[1].properties.type).toEqual({ type: 'string', enum: ['tool-call'] })
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('removes $schema property', async () => {
|
|
548
|
+
const adapter = new GoogleAdapter({
|
|
549
|
+
apiKey: 'test-key',
|
|
550
|
+
model: 'gemini-3-flash-preview'
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
554
|
+
|
|
555
|
+
await adapter.generate({
|
|
556
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
557
|
+
responseSchema: {
|
|
558
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: {
|
|
561
|
+
name: { type: 'string' }
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
maxOutputTokens: 1000
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
568
|
+
|
|
569
|
+
// Verify $schema was removed
|
|
570
|
+
expect(callParams.config.responseSchema).not.toHaveProperty('$schema')
|
|
571
|
+
expect(callParams.config.responseSchema.type).toBe('object')
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('inlines $ref definitions from $defs', async () => {
|
|
575
|
+
const adapter = new GoogleAdapter({
|
|
576
|
+
apiKey: 'test-key',
|
|
577
|
+
model: 'gemini-3-flash-preview'
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
581
|
+
|
|
582
|
+
// Schema with $ref and $defs
|
|
583
|
+
await adapter.generate({
|
|
584
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
585
|
+
responseSchema: {
|
|
586
|
+
type: 'object',
|
|
587
|
+
properties: {
|
|
588
|
+
item: { $ref: '#/$defs/Item' }
|
|
589
|
+
},
|
|
590
|
+
$defs: {
|
|
591
|
+
Item: {
|
|
592
|
+
type: 'object',
|
|
593
|
+
properties: {
|
|
594
|
+
id: { type: 'string' },
|
|
595
|
+
value: { type: 'number' }
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
maxOutputTokens: 1000
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
604
|
+
|
|
605
|
+
// Verify $ref was inlined
|
|
606
|
+
expect(callParams.config.responseSchema.properties.item).toEqual({
|
|
607
|
+
type: 'object',
|
|
608
|
+
properties: {
|
|
609
|
+
id: { type: 'string' },
|
|
610
|
+
value: { type: 'number' }
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
// Verify $defs was removed
|
|
614
|
+
expect(callParams.config.responseSchema).not.toHaveProperty('$defs')
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('handles deeply nested schemas with const', async () => {
|
|
618
|
+
const adapter = new GoogleAdapter({
|
|
619
|
+
apiKey: 'test-key',
|
|
620
|
+
model: 'gemini-3-flash-preview'
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
624
|
+
|
|
625
|
+
await adapter.generate({
|
|
626
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
627
|
+
responseSchema: {
|
|
628
|
+
type: 'object',
|
|
629
|
+
properties: {
|
|
630
|
+
data: {
|
|
631
|
+
type: 'array',
|
|
632
|
+
items: {
|
|
633
|
+
type: 'object',
|
|
634
|
+
properties: {
|
|
635
|
+
status: { const: 'active' }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
maxOutputTokens: 1000
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
645
|
+
|
|
646
|
+
// Verify nested const was transformed to type + enum
|
|
647
|
+
expect(callParams.config.responseSchema.properties.data.items.properties.status).toEqual({
|
|
648
|
+
type: 'string',
|
|
649
|
+
enum: ['active']
|
|
650
|
+
})
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('preserves other schema properties during transformation', async () => {
|
|
654
|
+
const adapter = new GoogleAdapter({
|
|
655
|
+
apiKey: 'test-key',
|
|
656
|
+
model: 'gemini-3-flash-preview'
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
660
|
+
|
|
661
|
+
await adapter.generate({
|
|
662
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
663
|
+
responseSchema: {
|
|
664
|
+
type: 'object',
|
|
665
|
+
properties: {
|
|
666
|
+
name: { type: 'string', description: 'User name', minLength: 1 },
|
|
667
|
+
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
668
|
+
role: { const: 'admin' }
|
|
669
|
+
},
|
|
670
|
+
required: ['name', 'role'],
|
|
671
|
+
additionalProperties: false
|
|
672
|
+
},
|
|
673
|
+
maxOutputTokens: 1000
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
677
|
+
const schema = callParams.config.responseSchema
|
|
678
|
+
|
|
679
|
+
// Verify structure preserved
|
|
680
|
+
expect(schema.type).toBe('object')
|
|
681
|
+
expect(schema.required).toEqual(['name', 'role'])
|
|
682
|
+
expect(schema.additionalProperties).toBe(false)
|
|
683
|
+
|
|
684
|
+
// Verify property details preserved
|
|
685
|
+
expect(schema.properties.name).toEqual({ type: 'string', description: 'User name', minLength: 1 })
|
|
686
|
+
expect(schema.properties.age).toEqual({ type: 'integer', minimum: 0, maximum: 150 })
|
|
687
|
+
|
|
688
|
+
// Verify const was transformed to type + enum
|
|
689
|
+
expect(schema.properties.role).toEqual({ type: 'string', enum: ['admin'] })
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
it('adds placeholder property to empty object schemas', async () => {
|
|
693
|
+
const adapter = new GoogleAdapter({
|
|
694
|
+
apiKey: 'test-key',
|
|
695
|
+
model: 'gemini-3-flash-preview'
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
mockGenerateContent.mockResolvedValueOnce(mockGoogleResponse)
|
|
699
|
+
|
|
700
|
+
await adapter.generate({
|
|
701
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
702
|
+
responseSchema: {
|
|
703
|
+
type: 'object',
|
|
704
|
+
properties: {
|
|
705
|
+
data: {
|
|
706
|
+
type: 'object',
|
|
707
|
+
properties: {} // Empty properties - Gemini rejects this
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
maxOutputTokens: 1000
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
const callParams = mockGenerateContent.mock.calls[0][0]
|
|
715
|
+
|
|
716
|
+
// Verify placeholder was added to empty object
|
|
717
|
+
// Uses 'placeholderField' - a name Google accepts (no underscore prefix)
|
|
718
|
+
expect(callParams.config.responseSchema.properties.data.properties).toHaveProperty('placeholderField')
|
|
719
|
+
expect(callParams.config.responseSchema.properties.data.properties.placeholderField.type).toBe('string')
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
})
|