Skip to content

Add AI Gateway client and text chat hook#343

Merged
IzumiSy merged 11 commits into
mainfrom
feat/1418-ai-gateway-phase1
Jul 2, 2026
Merged

Add AI Gateway client and text chat hook#343
IzumiSy merged 11 commits into
mainfrom
feat/1418-ai-gateway-phase1

Conversation

@IzumiSy

@IzumiSy IzumiSy commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Motivation

AppShell already has a browser auth stack that handles PKCE, token persistence, refresh, and DPoP-authenticated fetches through authClient.fetch(...). AI Gateway can accept DPoP-authenticated browser requests, so the missing piece for frontend consumers is not another auth layer but a small transport surface that normalizes AI Gateway responses into something AppShell apps can use directly.

This PR implements the first phase of the design discussed in tailor-inc/platform-planning#1418. That phase is intentionally limited to AppShell-owned primitives: a low-level AI Gateway client plus a simple text-only chat hook built on top of it. The optional Vercel AI SDK transport is left for a later phase so core can ship the smallest useful integration first.

Design Decision

Keep the client explicit

useAIChat(...) keeps the client explicit (useAIChat({ client, model })) instead of introducing an AIProvider in this phase.

That choice is intentional:

  • AuthProvider owns shared auth state and lifecycle concerns such as readiness, login/logout, token refresh, and guards, while AIGatewayClient is currently just a configured transport surface.
  • Keeping the client explicit makes the dependency visible at the call site, keeps tests simple, and naturally supports multiple clients or model selections without extra context wiring.
  • An AIProvider can still be added later as an additive convenience API if AppShell grows shared AI state, default models/prompts, or more composable chat UI primitives. For now, provider-first would add abstraction without much payoff.

Name the hook useAIChat

The hook is named useAIChat(...) rather than useChat(...) on purpose. Vercel AI SDK can own the generic useChat name because it is an AI-focused package, but AppShell is not. In AppShell, useChat would read like a generic chat primitive and overclaim the namespace for something that is specifically a wrapper around AI Gateway. useAIChat makes that dependency explicit and signals that the hook is for chatting with an AI model, not for arbitrary chat state in the design system.

Defer tool use

Tool use is also an important capability, but this PR intentionally keeps it out of scope for now. Instead, this change only leaves room in the type definitions so tool-related APIs can be added later without reshaping the core surface again.

That separation is intentional:

  • proper tool support would require a separate decision about how tool schemas should be defined and represented, for example JSON Schema, Zod, or another approach
  • that choice has implications for dependencies, API design, and long-term maintenance
  • those concerns are orthogonal to the transport and chat-hook surface this PR is trying to land

Summary

  • add createAIGatewayClient(...) to call AI Gateway via authClient.fetch(...), stream text deltas for OpenAI-compatible models, and normalize gemini-* responses into a single text chunk
  • add useAIChat(...) as a minimal text-only chat hook with local message state, streaming status, error handling, and abort support
  • export the new APIs from @tailor-platform/app-shell, document them, and add a changeset for the new feature

@IzumiSy IzumiSy requested a review from a team as a code owner June 29, 2026 08:14
@IzumiSy IzumiSy self-assigned this Jun 29, 2026
@IzumiSy IzumiSy force-pushed the feat/1418-ai-gateway-phase1 branch from 7b66bfa to c44d4a4 Compare June 29, 2026 08:44
@IzumiSy IzumiSy force-pushed the feat/1418-ai-gateway-phase1 branch 2 times, most recently from 21bcfc5 to a98b729 Compare June 30, 2026 05:00
@IzumiSy

IzumiSy commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

API Design Review completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by API Design Review for issue #343 · 77.8 AIC · ⌖ 7.18 AIC · ⊞ 5.8K
Comment /review to run again

Comment thread packages/core/src/ai/use-ai-chat.ts Outdated
Comment thread packages/core/src/ai/use-ai-chat.ts Outdated
@IzumiSy IzumiSy force-pushed the feat/1418-ai-gateway-phase1 branch 2 times, most recently from bcf887f to 4876302 Compare July 1, 2026 02:21
@IzumiSy IzumiSy force-pushed the feat/1418-ai-gateway-phase1 branch from ba25a8f to d0ed530 Compare July 1, 2026 03:10
Comment thread packages/core/src/ai/client.ts

@interacsean interacsean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical review — two Major findings. Details in inline comments.

Comment thread e2e/app/src/App.tsx
async *chatCompletionStream() {
throw new Error("AI Gateway not configured");
},
} satisfies AIGatewayClient;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chatCompletionStream doesn't exist on AIGatewayClient — the interface method is streamChatCompletion (packages/core/src/ai/client.ts:34). This is a leftover from the issue's original proposed API name. The satisfies AIGatewayClient here should be failing tsc; it only passes because the e2e package has no type-check/lint script, so the turbo run ... --filter=e2e CI step is a no-op for this package. It's dormant at runtime (the smoke handler early-returns when aiClient is null), but it'd throw streamChatCompletion is not a function if that guard ever changes.

const unavailableAIClient = {
  async *streamChatCompletion() {
    throw new Error("AI Gateway not configured");
  },
} satisfies AIGatewayClient;

Separately — worth adding a real type-check script to e2e/package.json so the CI step that claims to type-check this package actually does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd2e53. I renamed the fallback method to streamChatCompletion and added real lint/type-check scripts in e2e/package.json so turbo run lint type-check --filter=e2e is no longer a no-op.


yield* streamOpenAICompatibleResponse({
endpoint,
authClient: config.authClient,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routing is driven entirely by the caller's request.stream flag here — there's no gemini-* model detection. That diverges from issue #1418 §4 (Provider routing: gemini-* → JSON path, everything else → streamed), and from this PR's own Summary, which says it "normalize[s] gemini-* responses into a single text chunk."

Concrete impact: useAIChat({ model: "gemini-2.5-flash" }) defaults to stream: true → SSE path. If the gateway returns a single JSON body for Gemini (the premise behind the issue's routing rule), the SSE parser yields zero text-deltas and Gemini chat silently returns empty. The Gemini/JSON path is only covered in tests by manually passing stream: false, so this default path is untested.

Two options:

  1. Implement the model-based routing the issue specifies (model.startsWith("gemini-") → JSON path), or
  2. If the explicit stream flag is the intended replacement for model sniffing, keep it — but update the PR description and docs to state that provider routing is caller-driven and that useAIChat won't auto-select the JSON path for Gemini.

Either way this is a deliberate scope change from the issue and should be called out explicitly rather than described as automatic Gemini handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd2e53. useAIChat no longer forces stream: true, and the shared client now defaults gemini-* models to the JSON path when stream is unspecified while still honoring explicit stream: true/false overrides. I also added tests for the default Gemini route and the explicit override case.

@IzumiSy IzumiSy requested a review from interacsean July 2, 2026 06:05
@IzumiSy IzumiSy merged commit 116d2cc into main Jul 2, 2026
5 checks passed
@IzumiSy IzumiSy deleted the feat/1418-ai-gateway-phase1 branch July 2, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants