Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ jobs:
path: dist/

- name: Publish to PyPI
continue-on-error: true
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
skip-existing: true

publish-to-npm:
name: Publish to npm
Expand Down Expand Up @@ -195,45 +195,75 @@ jobs:

# pnpm pack resolves workspace: protocol refs, then npm publish uses OIDC
- name: Publish @cascadeflow/ml
continue-on-error: true
run: |
set -euo pipefail
cd packages/ml
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi
TARBALL=$(pnpm pack --pack-destination .)
npm publish "$TARBALL" --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cascadeflow/core
continue-on-error: true
run: |
set -euo pipefail
cd packages/core
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi
TARBALL=$(pnpm pack --pack-destination .)
npm publish "$TARBALL" --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cascadeflow/n8n-nodes-cascadeflow
continue-on-error: true
run: |
set -euo pipefail
cd packages/integrations/n8n
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi
TARBALL=$(pnpm pack --pack-destination .)
npm publish "$TARBALL" --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cascadeflow/langchain
continue-on-error: true
run: |
set -euo pipefail
cd packages/langchain-cascadeflow
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi
TARBALL=$(pnpm pack --pack-destination .)
npm publish "$TARBALL" --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cascadeflow/vercel-ai
continue-on-error: true
run: |
set -euo pipefail
cd packages/integrations/vercel-ai
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version > /dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi
TARBALL=$(pnpm pack --pack-destination .)
npm publish "$TARBALL" --access public --provenance
env:
Expand All @@ -258,8 +288,8 @@ jobs:
path: dist/

- name: Publish to TestPyPI
continue-on-error: true
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
skip-existing: true
190 changes: 190 additions & 0 deletions .github/workflows/vercel-deploy-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
name: Vercel Deploy E2E

on:
workflow_dispatch:
schedule:
- cron: '15 6 * * *'

jobs:
deployed-api-chat:
name: Deployed /api/chat E2E
runs-on: ubuntu-latest

steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8

- name: Check required secrets
id: secrets_check
shell: bash
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [[ -z "${VERCEL_TOKEN:-}" || -z "${OPENAI_API_KEY:-}" ]]; then
echo "missing=true" >> "$GITHUB_OUTPUT"
echo "Required secrets (VERCEL_TOKEN, OPENAI_API_KEY) are missing; skipping deploy E2E."
else
echo "missing=false" >> "$GITHUB_OUTPUT"
fi

- name: Deploy temporary app and test /api/chat
if: steps.secrets_check.outputs.missing == 'false'
shell: bash
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NEXT_TELEMETRY_DISABLED: '1'
run: |
set -euo pipefail

WORKDIR="$(mktemp -d)"
cd "$WORKDIR"
mkdir -p app/api/chat

cat > package.json <<'JSON'
{
"name": "cascadeflow-vercel-e2e",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "next build"
},
"dependencies": {
"@cascadeflow/core": "latest",
"@cascadeflow/vercel-ai": "latest",
"@ai-sdk/react": "^3.0.0",
"ai": "^6.0.0",
"next": "^16.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/react": "^19.2.4",
"@types/node": "^22.13.10"
}
}
JSON

cat > tsconfig.json <<'JSON'
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
JSON

cat > next-env.d.ts <<'TS'
/// <reference types="next" />
/// <reference types="next/image-types/global" />
TS

cat > app/page.tsx <<'TSX'
export default function Page() {
return <main>cascadeflow vercel e2e</main>;
}
TSX

cat > app/api/chat/route.ts <<'TS'
import { CascadeAgent } from '@cascadeflow/core';
import { createChatHandler } from '@cascadeflow/vercel-ai';

export const runtime = 'edge';

const agent = new CascadeAgent({
models: [
{ name: 'gpt-4o-mini', provider: 'openai', cost: 0.00015, apiKey: process.env.OPENAI_API_KEY },
{ name: 'gpt-4o', provider: 'openai', cost: 0.00625, apiKey: process.env.OPENAI_API_KEY }
]
});

const handler = createChatHandler(agent, { protocol: 'data' });

export async function POST(req: Request) {
return handler(req);
}
TS

PROJECT_NAME="cf-vercel-e2e-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
TEAM_QS=""
if [[ -n "${VERCEL_TEAM_ID:-}" ]]; then
TEAM_QS="?teamId=${VERCEL_TEAM_ID}"
fi

CREATE_RESP=$(curl -sS -X POST "https://api.vercel.com/v10/projects${TEAM_QS}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\":\"${PROJECT_NAME}\",\"framework\":\"nextjs\"}")
PROJECT_ID=$(node -e "const r=JSON.parse(process.argv[1]); if(!r.id){console.error(JSON.stringify(r)); process.exit(1)}; process.stdout.write(r.id);" "$CREATE_RESP")

cleanup() {
curl -sS -X DELETE "https://api.vercel.com/v9/projects/${PROJECT_ID}${TEAM_QS}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" >/dev/null || true
}
trap cleanup EXIT

# Disable deployment protection on the sandbox project so direct /api/chat checks work.
curl -sS -X PATCH "https://api.vercel.com/v10/projects/${PROJECT_ID}${TEAM_QS}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"ssoProtection":null}' >/dev/null

ENV_PAYLOAD=$(node -e "const key=process.argv[1]; console.log(JSON.stringify([{type:'encrypted',key:'OPENAI_API_KEY',value:key,target:['preview','production']}]));" "$OPENAI_API_KEY")
curl -sS -X POST "https://api.vercel.com/v10/projects/${PROJECT_ID}/env${TEAM_QS}" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
--data "$ENV_PAYLOAD" >/dev/null

pnpm install --silent

SCOPE_ARGS=()
if [[ -n "${VERCEL_SCOPE:-}" ]]; then
SCOPE_ARGS=(--scope "${VERCEL_SCOPE}")
fi

pnpm dlx vercel@latest link --yes --project "${PROJECT_NAME}" --token "${VERCEL_TOKEN}" "${SCOPE_ARGS[@]}" >/dev/null
DEPLOY_URL=$(pnpm dlx vercel@latest deploy --yes --token "${VERCEL_TOKEN}" "${SCOPE_ARGS[@]}" | tail -n 1 | tr -d '\r')
if [[ "$DEPLOY_URL" != http* ]]; then
DEPLOY_URL="https://${DEPLOY_URL}"
fi

RESP=$(curl -sS -X POST "${DEPLOY_URL}/api/chat" \
-H "content-type: application/json" \
--data '{"messages":[{"role":"user","content":"Reply with exactly: cascadeflow-ok"}]}')

TEXT=$(printf '%s' "$RESP" | awk -F'"' '/^0:/{printf "%s", $2}')
if [[ "$TEXT" != *"cascadeflow-ok"* ]]; then
echo "Unexpected response payload:"
echo "$RESP"
exit 1
fi

echo "Deployment URL: ${DEPLOY_URL}"
echo "Verified /api/chat streamed text: ${TEXT}"
6 changes: 5 additions & 1 deletion docs/guides/integrate_fast.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Why it’s fast:
- No provider rewrite.
- Compatible with `useChat` default `streamProtocol: 'data'` (AI SDK v4). On newer AI SDK versions, the handler automatically uses the UI message stream when available.

Deployment protection note:
- On some Vercel team projects, `ssoProtection` is enabled by default.
- If you validate `/api/chat` with direct `curl`, disable protection (or allow unauthenticated access) for your sandbox deployment first.

See: `examples/vercel-ai-nextjs/`.

## Option 2: “Keep Your SDK” via Proxy (Minimal Code Changes)
Expand All @@ -61,7 +65,7 @@ import { CascadeAgent } from '@cascadeflow/core';
const agent = new CascadeAgent({
models: [
{ name: 'gpt-4o-mini', provider: 'openai', cost: 0.00015, apiKey: process.env.OPENAI_API_KEY },
{ name: 'claude-sonnet-4-5-20250929', provider: 'anthropic', cost: 0.003, apiKey: process.env.ANTHROPIC_API_KEY },
{ name: 'claude-opus-4-6', provider: 'anthropic', cost: 0.015, apiKey: process.env.ANTHROPIC_API_KEY },
],
});

Expand Down
6 changes: 3 additions & 3 deletions docs/guides/langchain_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,14 @@ const verifier = new ChatOpenAI({ model: 'gpt-4o' });
```typescript
import { ChatAnthropic } from '@langchain/anthropic';

const drafter = new ChatAnthropic({ modelName: 'claude-3-haiku-20240307' });
const verifier = new ChatAnthropic({ modelName: 'claude-sonnet-4-6' });
const drafter = new ChatAnthropic({ modelName: 'claude-haiku-4-5-20251001' });
const verifier = new ChatAnthropic({ modelName: 'claude-opus-4-6' });
```

**Mix and Match:**
```typescript
// Cheap drafter (Haiku), powerful verifier (GPT-4o)
const drafter = new ChatAnthropic({ modelName: 'claude-3-haiku-20240307' });
const drafter = new ChatAnthropic({ modelName: 'claude-haiku-4-5-20251001' });
const verifier = new ChatOpenAI({ model: 'gpt-4o' });
```

Expand Down
23 changes: 23 additions & 0 deletions examples/vercel-ai-nextjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ This example shows how to use **cascadeflow** as the backend for the Vercel AI S
## Run

```bash
pnpm -C ../../packages/core build
pnpm -C ../../packages/integrations/vercel-ai build
cd examples/vercel-ai-nextjs
pnpm install
pnpm dev
```

`predev`/`prebuild` automatically build local workspace packages (`@cascadeflow/core` and `@cascadeflow/vercel-ai`) before running Next.js.

## Requirements

- `ai` + `@ai-sdk/react` already in your app (this example includes them).
Expand Down Expand Up @@ -45,6 +49,25 @@ vercel env add OPENAI_API_KEY preview
vercel deploy
```

### Deployment Protection (SSO) Note

On some Vercel team plans, newly created projects can default to deployment protection (`ssoProtection`).
If enabled, direct API probes to `/api/chat` can return `401` even when the route is healthy.
For sandbox E2E validation, disable protection on the sandbox project (or allow unauthenticated access for its domain).

### Real Deployed `/api/chat` Smoke Test

After deploy, validate the real network path (not only local tests):

```bash
DEPLOY_URL="https://<your-deployment>.vercel.app"
curl -sS -X POST "$DEPLOY_URL/api/chat" \
-H "content-type: application/json" \
--data '{"messages":[{"role":"user","content":"Reply with: cascadeflow-ok"}]}'
```

Expected result: a streaming response payload containing assistant text (for example `cascadeflow-ok`).

## How It Works

- `app/api/chat/route.ts` uses `@cascadeflow/vercel-ai` `createChatHandler(...)` with `protocol: 'data'`.
Expand Down
2 changes: 1 addition & 1 deletion examples/vercel-ai-nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 changes: 7 additions & 0 deletions examples/vercel-ai-nextjs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const nextConfig = {
// Next.js traces files correctly from the workspace root.
outputFileTracingRoot: path.join(__dirname, '../..'),
webpack: (config) => {
config.resolve = config.resolve ?? {};
config.resolve.alias = config.resolve.alias ?? {};
// Pin monorepo imports to local builds so example bundling does not
// accidentally resolve an older published package from the pnpm store.
config.resolve.alias['@cascadeflow/core'] = path.join(__dirname, '../../packages/core/dist/index.mjs');
config.resolve.alias['@cascadeflow/vercel-ai'] = path.join(__dirname, '../../packages/integrations/vercel-ai/dist/index.mjs');

// cascadeflow uses dynamic optional imports for integrations/providers.
// Next's webpack build warns about "dependency is an expression"; it's safe.
config.ignoreWarnings = config.ignoreWarnings ?? [];
Expand Down
Loading
Loading