Files
zeekay 83464f5d54 One default model on every surface: zen5
Office, Outlook, PDF, Clio, Meetings, Google Workspace and Salesforce
defaulted to zen-1, Teams offered zen-eco-3b, zen-mini-8b and zen-4b,
and JetBrains defaulted to claude-3-5-sonnet-20241022 with a fallback
list of retired upstream ids. None of those are in api.hanzo.ai/v1/models,
so a first request on each of them named a model the gateway does not
serve.

zen5 is the id the catalog itself marks as the default: catalog.hanzo.ai
/v1/pricing lists it as "Zen5 (Default)", "the everyday Zen5 chat model",
and nineteen other surfaces here already defaulted to it. Teams now offers
the catalog's Zen 5 family with zen5 first. JetBrains preselects zen5
when the live list carries it, and offers only zen5 when it cannot load
the list, rather than a fabricated one.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 12:29:56 -07:00
..
2026-08-29 13:46:57 -07:00

Hanzo AI for Salesforce

An SFDX package that puts Hanzo AI inside Salesforce โ€” a Lightning Web Component assistant that drops onto Account, Opportunity, Case, Lead, and Contact record pages. Summarize the record, draft a follow-up, extract next steps, or ask anything โ€” then write the result back as a Task, a Note, or a Chatter post.

It uses the same backend as everything else Hanzo: model calls go through api.hanzo.ai/v1 (OpenAI-compatible) โ€” the same gateway and wire format as the browser extension, the Office add-in, the Google Workspace add-on, and the Slack app. No new API surface, one contract everywhere.

Architecture

Salesforce runs on Apex (server-side) with Lightning Web Components (the client). Apex has no fetch/SSE, so every model call is one non-streaming completion (Http.send โ†’ render), which keeps the round-trip atomic โ€” the same one-shot model as the Apps Script and Office integrations.

force-app/main/default/
  classes/
    HanzoClient.cls          API client โ€” chatCompletionsUrl / buildMessages /
                             requestBody / extractContent / parseModels (pure,
                             tested) + ask / listModels (Http via Named Credential)
    HanzoContext.cls         SOQL context assembly per object (Account, Opportunity,
                             Case, Lead, Contact) + related lists + truncate
    HanzoController.cls       @AuraEnabled: complete / listModels / writeBack
    HanzoCalloutMock.cls      HttpCalloutMock for the api.hanzo.ai gateway (tests)
    HanzoClientTest.cls       wire contract + callout shape (>75% of HanzoClient)
    HanzoControllerTest.cls   complete/listModels/writeBack over real records
  lwc/hanzoAssistant/         the panel: model picker + quick actions + output +
                             prompt + write-back buttons
  namedCredentials/Hanzo_API      โ†’ https://api.hanzo.ai, injects the Bearer header
  externalCredentials/Hanzo_API   โ†’ holds the API-key principal (key entered post-deploy)
  permissionsets/Hanzo_AI_User    โ†’ Apex + External Credential principal access
  connectedApps/Hanzo_AI          โ†’ OAuth for programmatic access to this org
  customMetadata/ + objects/Hanzo_Config__mdt โ†’ non-secret default model config
config/project-scratch-def.json   scratch-org shape
src/context.js + tests/           the truncate helper mirrored for vitest
scripts/validate-source.mjs       structural preflight (no org needed)

The pure functions live once in HanzoClient and are the tested core, so the wire format is asserted the same way as every other Hanzo integration.

Security model โ€” where the key lives

The Hanzo API key is never in Apex, metadata, or git. It lives in the External Credential Hanzo_API as an authentication parameter, and the Named Credential injects it as Authorization: Bearer โ€ฆ at callout time via the merge field {!$Credential.Hanzo_API.ApiKey}. Apex only ever references callout:Hanzo_API โ€” it never sees the token. A user can call the assistant because their permission set grants External Credential principal access, not because they hold the key.

Non-secret configuration (the default model id) lives in the Hanzo_Config__mdt.Default custom-metadata record.

All record reads use WITH SECURITY_ENFORCED and all write-backs run as user, so the assistant honors the running user's field-level security and CRUD โ€” it can never surface or write data the user couldn't themselves.

Prerequisites

  • The Salesforce CLI (sf) on your PATH.
  • A Salesforce org (a scratch org, a Developer Edition, or a sandbox) with My Domain enabled (required for Named Credentials and LWC).
  • A Hanzo API key (sk-โ€ฆ) for your org โ€” mint one at hanzo.ai / your Hanzo IAM console.

Deploy

1. Deploy the source

# authorize a target org (once)
sf org login web --alias hanzo-sf

# validate + run tests before deploying (recommended)
npm run deploy:validate        # sf project deploy validate โ€ฆ RunLocalTests

# deploy
npm run deploy                 # sf project deploy start --source-dir force-app

For a fresh scratch org end-to-end:

npm run org:create             # create + set default scratch org
npm run org:push               # deploy the source
npm run org:open               # open it

2. Enter the Hanzo API key (once, per org โ€” the only manual step)

The deploy creates the Named + External Credential but not the key (secrets never ship in source). Add it once:

  1. Setup โ†’ Security โ†’ Named Credentials โ†’ External Credentials tab โ†’ Hanzo API.
  2. Under Principals, click New (or edit HanzoApiPrincipal):
    • Parameter Name: HanzoApiPrincipal
    • Sequence Number: 1
    • Under Authentication Parameters, add one named ApiKey with your sk-โ€ฆ key as the value.
  3. Save.

The Named Credential's header Bearer {!$Credential.Hanzo_API.ApiKey} now resolves to your key on every callout.

3. Grant access

Assign the Hanzo AI User permission set to the reps who should use the assistant (it grants the Apex + the External Credential principal access):

sf org assign permset --name Hanzo_AI_User

4. Add the LWC to a record page

  1. Open an Account (or Opportunity/Case/Lead/Contact) record.
  2. Setup (gear) โ†’ Edit Page (Lightning App Builder).
  3. Drag Hanzo AI Assistant from the Custom components list onto the page.
  4. Save โ†’ Activate (org default, app default, or by profile).

Repeat, or assign the same activation, for each object you want the panel on.

Using it

  • Pick a model in the panel header (loaded from /v1/models; falls back to the configured default, zen5).
  • Quick actions: Summarize ยท Draft email ยท Next steps ยท Risks.
  • Ask: type a question about the record and hit Ask.
  • Write back the output: Save as Task, Save as Note, or Post to Chatter โ€” created on the current record, in your user context.

Tests

Apex tests are the primary suite โ€” they use HttpCalloutMock (no live network) and assert the request shape (URL /v1/chat/completions, POST, body model + messages[] + stream:false), response parsing, and that each write-back creates the right record.

npm run apex:test              # sf apex run test โ€ฆ --code-coverage (needs an org)

A small vitest suite covers the pure JS truncate helper that mirrors the Apex context-truncation logic:

npm test                       # vitest run  (Node only, no org)

A structural preflight runs with no org at all โ€” it checks Apex meta/brace balance, that every LWC @salesforce/apex import resolves to an @AuraEnabled method, endpoint discipline (/v1 only, no /api/, no v2), and that no key or Bearer token is committed:

npm run validate               # node scripts/validate-source.mjs

Connected App / OAuth

The bundled Hanzo AI Connected App governs OAuth for programmatic access to this org (an external orchestrator calling in, or the org calling out under a named user). It is admin-approved and pre-authorized for the Hanzo AI User permission set. After deploy, retrieve its Consumer Key/Secret from Setup โ†’ App Manager โ†’ Hanzo AI โ†’ View. The model callout itself does not use this app โ€” it uses the Hanzo_API Named Credential.

AppExchange packaging

To ship this as a managed package (2GP) for AppExchange:

# create the package (once), then version it
sf package create --name "Hanzo AI for Salesforce" --package-type Managed --path force-app
sf package version create --package "Hanzo AI for Salesforce" \
  --installation-key-bypass --code-coverage --wait 30
sf package version promote --package "Hanzo AI for [email protected]"

The install link produced by version create installs the LWC, Apex, Named/ External Credential, permission set, and the config metadata into a subscriber org; the subscriber then does step 2 (enter their own sk-โ€ฆ key) and step 4 (add the LWC to their page layouts). Managed-package Apex requires โ‰ฅ75% coverage โ€” HanzoClientTest + HanzoControllerTest cover the callout, parsing, context assembly, and all three write-backs.