diff --git a/agents/base2/base2-kimi-2-7-code.ts b/agents/base2/base2-kimi-2-7-code.ts new file mode 100644 index 0000000000..d5b9af70d1 --- /dev/null +++ b/agents/base2/base2-kimi-2-7-code.ts @@ -0,0 +1,13 @@ +import { moonshotModels } from '@codebuff/common/constants/model-config' + +import { createBase2 } from './base2' + +const definition = { + ...createBase2('free', { + model: moonshotModels.kimiK27Code, + }), + id: 'base2-kimi-2-7-code', + displayName: 'Buffy the Kimi K2.7 Code Orchestrator', +} + +export default definition diff --git a/agents/types/agent-definition.ts b/agents/types/agent-definition.ts index 9355667dfd..6315dbeb17 100644 --- a/agents/types/agent-definition.ts +++ b/agents/types/agent-definition.ts @@ -436,6 +436,7 @@ export type ModelName = | 'moonshotai/kimi-k2' | 'moonshotai/kimi-k2:nitro' | 'moonshotai/kimi-k2.6' + | 'moonshotai/kimi-k2.7-code' | 'z-ai/glm-5' | 'z-ai/glm-5.1' | 'z-ai/glm-4.6' diff --git a/bun.lock b/bun.lock index ac102c862f..45c4d27af2 100644 --- a/bun.lock +++ b/bun.lock @@ -235,7 +235,7 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -587,7 +587,7 @@ "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], - "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], + "axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -595,7 +595,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.36", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], @@ -749,7 +749,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="], + "eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="], "eslint-config-prettier": ["eslint-config-prettier@9.1.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ=="], @@ -835,7 +835,7 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -1311,7 +1311,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-node": ["posthog-node@5.36.17", "", { "dependencies": { "@posthog/core": "1.32.3" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-ed1LT4a9hhiFJizB6XX7dkYYLVPAFHfUpkQSns7BRxoUyhFnvMq15QENKeAOUEKQgPmnaq2I+xNLdAHN0o9eAA=="], + "posthog-node": ["posthog-node@5.37.0", "", { "dependencies": { "@posthog/core": "^1.32.3" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-wFwWGcqAqZ1WJRlNNYc92veV83d1lOQcP4Lq0q7Kar9GdZLPpiFYHeudyybYJnjZjkI9v06vLvY/Og5CZIfByg=="], "preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="], @@ -1661,6 +1661,8 @@ "@opentui/core/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@types/diff/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="], diff --git a/common/src/__tests__/freebuff-referral-tiers.test.ts b/common/src/__tests__/freebuff-referral-tiers.test.ts new file mode 100644 index 0000000000..bcd280fc83 --- /dev/null +++ b/common/src/__tests__/freebuff-referral-tiers.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from 'bun:test' + +import { + FREEBUFF_REFERRAL_TIERS, + FREEBUFF_WATERMARK_REMOVAL_REFERRALS, + FREEBUFF_WATERMARK_REMOVAL_TIER, + MAX_FREEBUFF_REFERRAL_TIER, + MIN_GITHUB_ACCOUNT_AGE_MONTHS, + getNextReferralTier, + getReferralTier, + getTierLimits, + isGithubAccountOldEnoughForReferral, +} from '../constants/freebuff-referral-tiers' + +const NOW = Date.parse('2026-06-12T00:00:00Z') + +function monthsAgo(months: number): number { + const date = new Date(NOW) + date.setUTCMonth(date.getUTCMonth() - months) + return date.getTime() +} + +describe('FREEBUFF_REFERRAL_TIERS', () => { + it('is sorted ascending by referrals required with strictly growing limits', () => { + for (let i = 1; i < FREEBUFF_REFERRAL_TIERS.length; i++) { + const prev = FREEBUFF_REFERRAL_TIERS[i - 1] + const next = FREEBUFF_REFERRAL_TIERS[i] + expect(next.tier).toBe(prev.tier + 1) + expect(next.referralsRequired).toBeGreaterThan(prev.referralsRequired) + expect(next.standardModelDailyLimit).toBeGreaterThan( + prev.standardModelDailyLimit, + ) + expect(next.premiumModelDailyLimit).toBeGreaterThan( + prev.premiumModelDailyLimit, + ) + } + }) + + it('starts at tier 0 with 0 referrals and the watermark on', () => { + expect(FREEBUFF_REFERRAL_TIERS[0]).toMatchObject({ + tier: 0, + referralsRequired: 0, + removesWatermark: false, + }) + }) + + it('exposes the watermark unlock tier', () => { + expect(FREEBUFF_WATERMARK_REMOVAL_TIER).toBe(1) + expect(FREEBUFF_WATERMARK_REMOVAL_REFERRALS).toBe(1) + }) + + it('follows the 1 / +2 (3) / +4 (7) referral ladder', () => { + expect(FREEBUFF_REFERRAL_TIERS.map((t) => t.referralsRequired)).toEqual([ + 0, 1, 3, 7, + ]) + }) +}) + +describe('getReferralTier', () => { + it('maps counts to the highest unlocked tier', () => { + expect(getReferralTier(0).tier).toBe(0) + expect(getReferralTier(1).tier).toBe(1) + expect(getReferralTier(2).tier).toBe(1) + expect(getReferralTier(3).tier).toBe(2) + expect(getReferralTier(6).tier).toBe(2) + expect(getReferralTier(7).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER) + expect(getReferralTier(100).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER) + }) + + it('treats null/undefined/negative counts as tier 0', () => { + expect(getReferralTier(null).tier).toBe(0) + expect(getReferralTier(undefined).tier).toBe(0) + expect(getReferralTier(-3).tier).toBe(0) + }) +}) + +describe('getTierLimits', () => { + it('returns the tier row and clamps out-of-range tiers', () => { + expect(getTierLimits(1).standardModelDailyLimit).toBe( + FREEBUFF_REFERRAL_TIERS[1].standardModelDailyLimit, + ) + expect(getTierLimits(-1).tier).toBe(0) + expect(getTierLimits(99).tier).toBe(MAX_FREEBUFF_REFERRAL_TIER) + }) +}) + +describe('getNextReferralTier', () => { + it('returns the next tier and null when maxed', () => { + expect(getNextReferralTier(0)?.tier).toBe(1) + expect(getNextReferralTier(1)?.tier).toBe(2) + expect(getNextReferralTier(3)?.tier).toBe(3) + expect(getNextReferralTier(7)).toBeNull() + }) +}) + +describe('isGithubAccountOldEnoughForReferral', () => { + it('accepts accounts at or beyond the age threshold', () => { + expect( + isGithubAccountOldEnoughForReferral( + monthsAgo(MIN_GITHUB_ACCOUNT_AGE_MONTHS), + NOW, + ), + ).toBe(true) + expect(isGithubAccountOldEnoughForReferral(monthsAgo(36), NOW)).toBe(true) + }) + + it('rejects accounts younger than the threshold', () => { + expect( + isGithubAccountOldEnoughForReferral( + monthsAgo(MIN_GITHUB_ACCOUNT_AGE_MONTHS - 1), + NOW, + ), + ).toBe(false) + expect(isGithubAccountOldEnoughForReferral(NOW, NOW)).toBe(false) + }) + + it('rejects missing or invalid creation dates', () => { + expect(isGithubAccountOldEnoughForReferral(null, NOW)).toBe(false) + expect(isGithubAccountOldEnoughForReferral(undefined, NOW)).toBe(false) + expect(isGithubAccountOldEnoughForReferral(Number.NaN, NOW)).toBe(false) + }) +}) diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts index 971096f964..765a4ba02e 100644 --- a/common/src/constants/freebuff-models.ts +++ b/common/src/constants/freebuff-models.ts @@ -4,7 +4,7 @@ import { getZonedParts, type ZonedDateParts, } from '../util/zoned-time' -import { mimoModels, minimaxModels } from './model-config' +import { mimoModels, minimaxModels, moonshotModels } from './model-config' /** * Models a freebuff user can pick between in the waiting-room model selector. @@ -50,7 +50,7 @@ export const FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID = 'deepseek/deepseek-v4-flash' * the free-mode allowlists — the CLI and web builder keep DeepSeek direct. */ export const FREEBUFF_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID = 'fireworks/deepseek-v4-flash' -export const FREEBUFF_KIMI_MODEL_ID = 'moonshotai/kimi-k2.6' +export const FREEBUFF_KIMI_MODEL_ID = moonshotModels.kimiK26 /** Legacy: removed from the pickers on 2026-06-09 in favor of MiniMax M3, but * still server-supported so old clients keep working. Drop from * SUPPORTED_FREEBUFF_MODELS after ~2026-06-16. */ diff --git a/common/src/constants/freebuff-referral-tiers.ts b/common/src/constants/freebuff-referral-tiers.ts new file mode 100644 index 0000000000..4eb80b412a --- /dev/null +++ b/common/src/constants/freebuff-referral-tiers.ts @@ -0,0 +1,129 @@ +/** + * Freebuff Web referral tiers. + * + * Each *qualified* referral (referred user's GitHub account is at least + * MIN_GITHUB_ACCOUNT_AGE_MONTHS old at signup) raises the referrer's tier. + * Tiers scale the daily model usage limits and unlock perks (deploy + * watermark removal). In full/allowed regions both standard and premium + * limits apply; in limited regions, users can still unlock tiers but only the + * standard/free-model limit applies because premium models remain geo-gated. + * All tunable numbers live in this file. + */ + +/** Referred users must have a GitHub account at least this old for the + * referral to count. Younger accounts can still sign up normally — the + * referrer just gets no credit (anti-farming). */ +export const MIN_GITHUB_ACCOUNT_AGE_MONTHS = 4 + +export interface FreebuffReferralTier { + /** Tier index (0-based, in ascending order of referralsRequired). */ + tier: number + /** Qualified referrals needed to reach this tier. */ + referralsRequired: number + /** Daily message cap for standard (non-premium) models. */ + standardModelDailyLimit: number + /** Daily message cap for premium models. */ + premiumModelDailyLimit: number + /** Whether the "Powered by Freebuff" watermark is removed from deploys. */ + removesWatermark: boolean +} + +/** Tier ladder: 1 referral, then +2 (3 total), then +4 (7 total). */ +export const FREEBUFF_REFERRAL_TIERS: readonly FreebuffReferralTier[] = [ + { + tier: 0, + referralsRequired: 0, + standardModelDailyLimit: 20, + premiumModelDailyLimit: 3, + removesWatermark: false, + }, + { + tier: 1, + referralsRequired: 1, + standardModelDailyLimit: 30, + premiumModelDailyLimit: 4, + removesWatermark: true, + }, + { + tier: 2, + referralsRequired: 3, + standardModelDailyLimit: 50, + premiumModelDailyLimit: 6, + removesWatermark: true, + }, + { + tier: 3, + referralsRequired: 7, + standardModelDailyLimit: 80, + premiumModelDailyLimit: 10, + removesWatermark: true, + }, +] as const + +/** + * Max attributed web signups (pending + completed) per referrer. The shared + * `user.referral_limit` column (default 5) governs the CLI program; the web + * ladder tops out at 7 qualified referrals, so it needs its own headroom — + * generous enough for unqualified signups, small enough to bound farming. + */ +export const FREEBUFF_WEB_REFERRAL_LIMIT = 20 + +export const MAX_FREEBUFF_REFERRAL_TIER = + FREEBUFF_REFERRAL_TIERS[FREEBUFF_REFERRAL_TIERS.length - 1].tier + +/** Lowest tier whose perks include watermark removal. */ +export const FREEBUFF_WATERMARK_REMOVAL_TIER = FREEBUFF_REFERRAL_TIERS.find( + (tier) => tier.removesWatermark, +)!.tier + +/** Qualified referrals needed before deploys drop the watermark. */ +export const FREEBUFF_WATERMARK_REMOVAL_REFERRALS = + FREEBUFF_REFERRAL_TIERS.find( + (tier) => tier.removesWatermark, + )!.referralsRequired + +/** Highest tier unlocked by the given qualified referral count. */ +export function getReferralTier( + qualifiedReferralCount: number | null | undefined, +): FreebuffReferralTier { + const count = Math.max(0, qualifiedReferralCount ?? 0) + let unlocked = FREEBUFF_REFERRAL_TIERS[0] + for (const tier of FREEBUFF_REFERRAL_TIERS) { + if (count >= tier.referralsRequired) { + unlocked = tier + } + } + return unlocked +} + +/** Tier limits by tier index (clamped into range). */ +export function getTierLimits(tier: number): FreebuffReferralTier { + const clamped = Math.min(Math.max(0, tier), MAX_FREEBUFF_REFERRAL_TIER) + return FREEBUFF_REFERRAL_TIERS.find((t) => t.tier === clamped)! +} + +/** Next tier above the given qualified referral count, or null if maxed. */ +export function getNextReferralTier( + qualifiedReferralCount: number | null | undefined, +): FreebuffReferralTier | null { + const current = getReferralTier(qualifiedReferralCount) + return ( + FREEBUFF_REFERRAL_TIERS.find((tier) => tier.tier === current.tier + 1) ?? + null + ) +} + +/** Whether a GitHub account created at `githubCreatedAtMs` satisfies the + * referral age requirement at time `nowMs`. Months are computed on the + * calendar (e.g. created Jan 15 qualifies on/after May 15). */ +export function isGithubAccountOldEnoughForReferral( + githubCreatedAtMs: number | null | undefined, + nowMs: number = Date.now(), +): boolean { + if (githubCreatedAtMs == null || !Number.isFinite(githubCreatedAtMs)) { + return false + } + const threshold = new Date(githubCreatedAtMs) + threshold.setUTCMonth(threshold.getUTCMonth() + MIN_GITHUB_ACCOUNT_AGE_MONTHS) + return nowMs >= threshold.getTime() +} diff --git a/common/src/constants/model-config.ts b/common/src/constants/model-config.ts index f9b8d3f5fc..7219d14703 100644 --- a/common/src/constants/model-config.ts +++ b/common/src/constants/model-config.ts @@ -85,6 +85,13 @@ export const minimaxModels = { } as const export type MiniMaxModel = (typeof minimaxModels)[keyof typeof minimaxModels] +export const moonshotModels = { + kimiK26: 'moonshotai/kimi-k2.6', + kimiK27Code: 'moonshotai/kimi-k2.7-code', +} as const +export type MoonshotModel = + (typeof moonshotModels)[keyof typeof moonshotModels] + // Vertex uses "endpoint IDs" for finetuned models, which are just integers export const finetunedVertexModels = { ft_filepicker_003: '196166068534771712', diff --git a/common/src/templates/initial-agents-dir/types/agent-definition.ts b/common/src/templates/initial-agents-dir/types/agent-definition.ts index 9355667dfd..6315dbeb17 100644 --- a/common/src/templates/initial-agents-dir/types/agent-definition.ts +++ b/common/src/templates/initial-agents-dir/types/agent-definition.ts @@ -436,6 +436,7 @@ export type ModelName = | 'moonshotai/kimi-k2' | 'moonshotai/kimi-k2:nitro' | 'moonshotai/kimi-k2.6' + | 'moonshotai/kimi-k2.7-code' | 'z-ai/glm-5' | 'z-ai/glm-5.1' | 'z-ai/glm-4.6' diff --git a/evals/buffbench/main-single-eval.ts b/evals/buffbench/main-single-eval.ts index bff2d322bf..8bae5cd7e4 100644 --- a/evals/buffbench/main-single-eval.ts +++ b/evals/buffbench/main-single-eval.ts @@ -7,7 +7,7 @@ async function main() { await runBuffBench({ evalDataPaths: [path.join(__dirname, 'eval-codebuff.json')], - agents: ['base2-free-deepseek-v4'], + agents: ['base2-kimi-2-7-code'], taskIds: ['server-agent-validation'], saveTraces, }) diff --git a/packages/agent-runtime/src/__tests__/gravity-index-tool.test.ts b/packages/agent-runtime/src/__tests__/gravity-index-tool.test.ts index 3f96efaa8c..e4260f590c 100644 --- a/packages/agent-runtime/src/__tests__/gravity-index-tool.test.ts +++ b/packages/agent-runtime/src/__tests__/gravity-index-tool.test.ts @@ -202,6 +202,60 @@ describe('gravity_index tool', () => { ) }) + test('does not special-case base2-free traffic as web surface', async () => { + const spy = spyOn(webApi, 'callGravityIndexAPI').mockResolvedValue({ + result: { search_id: 'search-1' }, + }) + + mockAgentStream([ + createToolCallChunk('gravity_index', { + action: 'search', + query: 'transactional email for Next.js', + }), + createToolCallChunk('end_turn', {}), + ]) + + const fileContext = { + ...mockFileContext, + agentTemplates: { + 'base2-free-deepseek': { + ...gravityTestAgent, + id: 'base2-free-deepseek', + displayName: 'Buffy the DeepSeek Free Orchestrator', + }, + }, + } + const sessionState = getInitialSessionState(fileContext) + const agentState = { + ...sessionState.mainAgentState, + agentType: 'base2-free-deepseek', + } + const { agentTemplates } = assembleLocalAgentTemplates({ + ...agentRuntimeImpl, + fileContext, + }) + + await runAgentStep({ + ...runAgentStepBaseParams, + agentType: 'base2-free-deepseek', + fileContext, + localAgentTemplates: agentTemplates, + agentTemplate: agentTemplates['base2-free-deepseek'], + agentState, + prompt: 'Find an email provider', + }) + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ + metadata: expect.objectContaining({ + surface: 'codebuff_cli', + }), + }), + }), + ) + }) + test('stores recommendation and setup URL in tool output', async () => { spyOn(webApi, 'callGravityIndexAPI').mockResolvedValue({ result: { diff --git a/sdk/src/__tests__/researcher-web.integration.test.ts b/sdk/src/__tests__/researcher-web.integration.test.ts index a5e981654a..a1d96f0c30 100644 --- a/sdk/src/__tests__/researcher-web.integration.test.ts +++ b/sdk/src/__tests__/researcher-web.integration.test.ts @@ -12,6 +12,7 @@ import type { PrintModeEvent } from '@codebuff/common/types/print-mode' const DEFAULT_TIMEOUT_MS = 120_000 const EXPECTED_KEYWORD = 'useActionState' +const RESEARCHER_WEB_MAX_AGENT_STEPS = 10 function loadEnvValue(name: string): string | undefined { if (process.env[name] && process.env[name] !== 'test') { @@ -158,13 +159,13 @@ describe('researcher-web SDK integration', () => { const result = await client.run({ agent: 'researcher-web', agentDefinitions: [researcherWeb], - maxAgentSteps: 8, + maxAgentSteps: RESEARCHER_WEB_MAX_AGENT_STEPS, handleEvent: (event) => { events.push(event) }, prompt: [ 'Use web search to answer this React docs question.', - 'After searching, fetch the most relevant React docs page with read_url before answering.', + 'After searching, fetch exactly three relevant React docs pages with read_url before answering.', 'In React 19, which hook returns state, a form action, and an isPending value for form actions?', 'Answer with the exact hook name and one short sentence.', ].join(' '),